merge with default branch

This commit is contained in:
Sebastian Sdorra
2019-12-05 16:14:44 +01:00
166 changed files with 2566 additions and 704 deletions

2
Jenkinsfile vendored
View File

@@ -7,7 +7,7 @@ import com.cloudogu.ces.cesbuildlib.*
node('docker') { node('docker') {
// Change this as when we go back to default - necessary for proper SonarQube analysis // Change this as when we go back to default - necessary for proper SonarQube analysis
mainBranch = '2.0.0-m3' mainBranch = 'default'
properties([ properties([
// Keep only the last 10 build to preserve space // Keep only the last 10 build to preserve space

View File

@@ -11,7 +11,8 @@
"test": "lerna run --scope '@scm-manager/ui-*' test", "test": "lerna run --scope '@scm-manager/ui-*' test",
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck", "typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
"serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js", "serve": "webpack-dev-server --mode=development --config=scm-ui/ui-scripts/src/webpack.config.js",
"deploy": "ui-scripts publish" "deploy": "ui-scripts publish",
"set-version": "ui-scripts version"
}, },
"devDependencies": { "devDependencies": {
"babel-plugin-reflow": "^0.2.7", "babel-plugin-reflow": "^0.2.7",

View File

@@ -99,8 +99,9 @@ import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamResult;
import static javax.lang.model.util.ElementFilter.methodsIn;
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@SupportedAnnotationTypes("*") @SupportedAnnotationTypes("*")
@@ -372,6 +373,18 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
attributes.put(entry.getKey().getSimpleName().toString(), attributes.put(entry.getKey().getSimpleName().toString(),
getValue(entry.getValue())); getValue(entry.getValue()));
} }
// add default values
for (ExecutableElement meth : methodsIn(annotationMirror.getAnnotationType().asElement().getEnclosedElements())) {
String attribute = meth.getSimpleName().toString();
AnnotationValue defaultValue = meth.getDefaultValue();
if (defaultValue != null && !attributes.containsKey(attribute)) {
String value = getValue(defaultValue);
if (value != null && !value.isEmpty()) {
attributes.put(attribute, value);
}
}
}
} }
} }

View File

@@ -9,7 +9,6 @@
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
</parent> </parent>
<groupId>sonia.scm</groupId>
<artifactId>scm-annotations</artifactId> <artifactId>scm-annotations</artifactId>
<version>2.0.0-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
<name>scm-annotations</name> <name>scm-annotations</name>

View File

@@ -17,7 +17,7 @@ public class ConcurrentModificationException extends ExceptionWithContext {
this(Collections.singletonList(new ContextEntry(type, id))); this(Collections.singletonList(new ContextEntry(type, id)));
} }
private ConcurrentModificationException(List<ContextEntry> context) { public ConcurrentModificationException(List<ContextEntry> context) {
super(context, createMessage(context)); super(context, createMessage(context));
} }
@@ -32,3 +32,4 @@ public class ConcurrentModificationException extends ExceptionWithContext {
.collect(joining(" in ", "", " has been modified concurrently")); .collect(joining(" in ", "", " has been modified concurrently"));
} }
} }

View File

@@ -59,7 +59,7 @@ public final class SCMContext
*/ */
public static final User ANONYMOUS = new User(USER_ANONYMOUS, public static final User ANONYMOUS = new User(USER_ANONYMOUS,
"SCM Anonymous", "SCM Anonymous",
"scm-anonymous@scm-manager.com"); "scm-anonymous@scm-manager.org");
/** Singleton instance of {@link SCMContextProvider} */ /** Singleton instance of {@link SCMContextProvider} */
private static volatile SCMContextProvider provider; private static volatile SCMContextProvider provider;

View File

@@ -134,7 +134,7 @@ public final class IterableQueue<T> implements Iterable<T>
else else
{ {
logger.trace("create queue iterator"); logger.trace("create queue iterator");
iterator = new QueueIterator<T>(this); iterator = new QueueIterator<>(this);
} }
return iterator; return iterator;

View File

@@ -63,7 +63,7 @@ public class LimitedSortedSet<E> extends ForwardingSortedSet<E>
*/ */
public LimitedSortedSet(int maxSize) public LimitedSortedSet(int maxSize)
{ {
this.sortedSet = new TreeSet<E>(); this.sortedSet = new TreeSet<>();
this.maxSize = maxSize; this.maxSize = maxSize;
} }

View File

@@ -183,5 +183,5 @@ public abstract class AbstractResourceProcessor implements ResourceProcessor
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
private Map<String, String> variableMap = new HashMap<String, String>(); private Map<String, String> variableMap = new HashMap<>();
} }

View File

@@ -52,7 +52,7 @@ public class INIConfiguration
*/ */
public INIConfiguration() public INIConfiguration()
{ {
this.sectionMap = new LinkedHashMap<String, INISection>(); this.sectionMap = new LinkedHashMap<>();
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -55,7 +55,7 @@ public class INISection
public INISection(String name) public INISection(String name)
{ {
this.name = name; this.name = name;
this.parameters = new LinkedHashMap<String, String>(); this.parameters = new LinkedHashMap<>();
} }
/** /**

View File

@@ -25,7 +25,7 @@ public class AvailablePlugin implements Plugin {
return pending; return pending;
} }
public AvailablePlugin install() { AvailablePlugin install() {
Preconditions.checkState(!pending, "installation is already pending"); Preconditions.checkState(!pending, "installation is already pending");
return new AvailablePlugin(pluginDescriptor, true); return new AvailablePlugin(pluginDescriptor, true);
} }

View File

@@ -113,7 +113,6 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler
Repository repository = new Repository(); Repository repository = new Repository();
repository.setName(repositoryName); repository.setName(repositoryName);
repository.setPublicReadable(false);
repository.setType(getTypeName()); repository.setType(getTypeName());
return repository; return repository;

View File

@@ -83,8 +83,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private String name; private String name;
@XmlElement(name = "permission") @XmlElement(name = "permission")
private Set<RepositoryPermission> permissions = new HashSet<>(); private Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private String type; private String type;
@@ -225,15 +223,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return Util.isEmpty(healthCheckFailures); return Util.isEmpty(healthCheckFailures);
} }
/**
* Returns true if the {@link Repository} is public readable.
*
* @return true if the {@link Repository} is public readable
*/
public boolean isPublicReadable() {
return publicReadable;
}
/** /**
* Returns true if the {@link Repository} is valid. * Returns true if the {@link Repository} is valid.
* <ul> * <ul>
@@ -292,10 +281,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return this.permissions.remove(permission); return this.permissions.remove(permission);
} }
public void setPublicReadable(boolean publicReadable) {
this.publicReadable = publicReadable;
}
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
@@ -332,7 +317,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
repository.setLastModified(lastModified); repository.setLastModified(lastModified);
repository.setDescription(description); repository.setDescription(description);
repository.setPermissions(permissions); repository.setPermissions(permissions);
repository.setPublicReadable(publicReadable);
// do not copy health check results // do not copy health check results
} }
@@ -360,7 +344,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
&& Objects.equal(name, other.name) && Objects.equal(name, other.name)
&& Objects.equal(contact, other.contact) && Objects.equal(contact, other.contact)
&& Objects.equal(description, other.description) && Objects.equal(description, other.description)
&& Objects.equal(publicReadable, other.publicReadable)
&& Objects.equal(permissions, other.permissions) && Objects.equal(permissions, other.permissions)
&& Objects.equal(type, other.type) && Objects.equal(type, other.type)
&& Objects.equal(creationDate, other.creationDate) && Objects.equal(creationDate, other.creationDate)
@@ -371,7 +354,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode(id, namespace, name, contact, description, publicReadable, return Objects.hashCode(id, namespace, name, contact, description,
permissions, type, creationDate, lastModified, properties, permissions, type, creationDate, lastModified, properties,
healthCheckFailures); healthCheckFailures);
} }
@@ -384,7 +367,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
.add("name", name) .add("name", name)
.add("contact", contact) .add("contact", contact)
.add("description", description) .add("description", description)
.add("publicReadable", publicReadable)
.add("permissions", permissions) .add("permissions", permissions)
.add("type", type) .add("type", type)
.add("lastModified", lastModified) .add("lastModified", lastModified)

View File

@@ -46,7 +46,7 @@ public final class Base32 extends AbstractBase
{ {
/** base value */ /** base value */
private static final BigInteger BASE = BigInteger.valueOf(32l); private static final BigInteger BASE = BigInteger.valueOf(32L);
/** char table */ /** char table */
private static final String CHARS = "0123456789bcdefghjkmnpqrstuvwxyz"; private static final String CHARS = "0123456789bcdefghjkmnpqrstuvwxyz";

View File

@@ -46,7 +46,7 @@ public final class Base62 extends AbstractBase
{ {
/** base value */ /** base value */
private static final BigInteger BASE = BigInteger.valueOf(62l); private static final BigInteger BASE = BigInteger.valueOf(62L);
/** char table */ /** char table */
private static final String CHARS = private static final String CHARS =

View File

@@ -79,7 +79,7 @@ public final class LinkTextParser
public static String parseText(String content) public static String parseText(String content)
{ {
Matcher m = REGEX_URL.matcher(content); Matcher m = REGEX_URL.matcher(content);
List<Token> tokens = new ArrayList<Token>(); List<Token> tokens = new ArrayList<>();
int position = 0; int position = 0;
String tokenContent = null; String tokenContent = null;

View File

@@ -119,7 +119,7 @@ public final class ServiceUtil
*/ */
public static <T> List<T> getServices(Class<T> type) public static <T> List<T> getServices(Class<T> type)
{ {
List<T> result = new ArrayList<T>(); List<T> result = new ArrayList<>();
try try
{ {

View File

@@ -38,23 +38,12 @@ package sonia.scm.util;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
//~--- JDK imports ------------------------------------------------------------
import java.math.BigInteger; import java.math.BigInteger;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*;
import java.util.ArrayList; //~--- JDK imports ------------------------------------------------------------
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/** /**
* *
@@ -273,12 +262,12 @@ public final class Util
Comparator<T> comparator, CollectionAppender<T> appender, int start, Comparator<T> comparator, CollectionAppender<T> appender, int start,
int limit) int limit)
{ {
List<T> result = new ArrayList<T>(); List<T> result = new ArrayList<>();
List<T> valueList = new ArrayList<T>(values); List<T> valueList = new ArrayList<>(values);
if (comparator != null) if (comparator != null)
{ {
Collections.sort(valueList, comparator); valueList.sort(comparator);
} }
int length = valueList.size(); int length = valueList.size();
@@ -506,12 +495,10 @@ public final class Util
{ {
StringBuilder buffer = new StringBuilder(); StringBuilder buffer = new StringBuilder();
for (int i = 0; i < byteValue.length; i++) for (final byte aByteValue : byteValue) {
{ int x = aByteValue & 0xff;
int x = byteValue[i] & 0xff;
if (x < 16) if (x < 16) {
{
buffer.append('0'); buffer.append('0');
} }

View File

@@ -66,7 +66,7 @@ public class EnvList
*/ */
public EnvList() public EnvList()
{ {
envMap = new HashMap<String, String>(); envMap = new HashMap<>();
} }
/** /**
@@ -77,7 +77,7 @@ public class EnvList
*/ */
public EnvList(EnvList l) public EnvList(EnvList l)
{ {
envMap = new HashMap<String, String>(l.envMap); envMap = new HashMap<>(l.envMap);
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -127,7 +127,7 @@ public class AuthenticationFilter extends HttpFilter
logger.trace("user is already authenticated"); logger.trace("user is already authenticated");
processChain(request, response, chain, subject); processChain(request, response, chain, subject);
} }
else if (isAnonymousAccessEnabled()) else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request))
{ {
logger.trace("anonymous access granted"); logger.trace("anonymous access granted");
subject.login(new AnonymousToken()); subject.login(new AnonymousToken());

View File

@@ -470,13 +470,13 @@ public class BufferedHttpServletResponse extends HttpServletResponseWrapper
private ByteArrayPrintWriter pw = null; private ByteArrayPrintWriter pw = null;
/** Field description */ /** Field description */
private Set<Cookie> cookies = new HashSet<Cookie>(); private Set<Cookie> cookies = new HashSet<>();
/** Field description */ /** Field description */
private int statusCode = HttpServletResponse.SC_OK; private int statusCode = HttpServletResponse.SC_OK;
/** Field description */ /** Field description */
private Map<String, String> headers = new LinkedHashMap<String, String>(); private Map<String, String> headers = new LinkedHashMap<>();
/** Field description */ /** Field description */
private String statusMessage; private String statusMessage;

View File

@@ -40,28 +40,20 @@ import com.google.common.io.ByteStreams;
import com.google.common.io.Closer; import com.google.common.io.Closer;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
//~--- JDK imports ------------------------------------------------------------ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedInputStream; import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream; import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import javax.servlet.http.HttpServlet; //~--- JDK imports ------------------------------------------------------------
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** /**
* *
@@ -175,10 +167,8 @@ public class ProxyServlet extends HttpServlet
private void copyContent(HttpURLConnection con, HttpServletResponse response) private void copyContent(HttpURLConnection con, HttpServletResponse response)
throws IOException throws IOException
{ {
Closer closer = Closer.create();
try try (Closer closer = Closer.create()) {
{
InputStream webToProxyBuf = InputStream webToProxyBuf =
closer.register(new BufferedInputStream(con.getInputStream())); closer.register(new BufferedInputStream(con.getInputStream()));
OutputStream proxyToClientBuf = OutputStream proxyToClientBuf =
@@ -188,10 +178,6 @@ public class ProxyServlet extends HttpServlet
logger.trace("copied {} bytes for proxy", bytes); logger.trace("copied {} bytes for proxy", bytes);
} }
finally
{
closer.close();
}
} }
/** /**

View File

@@ -102,7 +102,7 @@ public class XmlMapStringAdapter
public Map<String, String> unmarshal(XmlMapStringElement[] elements) public Map<String, String> unmarshal(XmlMapStringElement[] elements)
throws Exception throws Exception
{ {
Map<String, String> map = new HashMap<String, String>(); Map<String, String> map = new HashMap<>();
if (elements != null) if (elements != null)
{ {

View File

@@ -90,7 +90,7 @@ public class XmlSetStringAdapter extends XmlAdapter<String, Set<String>>
@Override @Override
public Set<String> unmarshal(String rawString) throws Exception public Set<String> unmarshal(String rawString) throws Exception
{ {
Set<String> tokens = new HashSet<String>(); Set<String> tokens = new HashSet<>();
for (String token : rawString.split(",")) for (String token : rawString.split(","))
{ {

View File

@@ -63,7 +63,7 @@ public class IterableQueueTest
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void testDuplicatedEndReached() public void testDuplicatedEndReached()
{ {
IterableQueue<String> queue = new IterableQueue<String>(); IterableQueue<String> queue = new IterableQueue<>();
queue.endReached(); queue.endReached();
queue.endReached(); queue.endReached();
@@ -76,7 +76,7 @@ public class IterableQueueTest
@Test @Test
public void testIterator() public void testIterator()
{ {
IterableQueue<String> queue = new IterableQueue<String>(); IterableQueue<String> queue = new IterableQueue<>();
assertEquals(QueueIterator.class, queue.iterator().getClass()); assertEquals(QueueIterator.class, queue.iterator().getClass());
queue.endReached(); queue.endReached();
@@ -120,7 +120,7 @@ public class IterableQueueTest
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void testPushEndReached() public void testPushEndReached()
{ {
IterableQueue<String> queue = new IterableQueue<String>(); IterableQueue<String> queue = new IterableQueue<>();
queue.push("a"); queue.push("a");
queue.endReached(); queue.endReached();
@@ -134,7 +134,7 @@ public class IterableQueueTest
@Test @Test
public void testSingleConsumer() public void testSingleConsumer()
{ {
final IterableQueue<Integer> queue = new IterableQueue<Integer>(); final IterableQueue<Integer> queue = new IterableQueue<>();
new Thread(new IntegerProducer(queue, false, 100)).start(); new Thread(new IntegerProducer(queue, false, 100)).start();
assertResult(Lists.newArrayList(queue), 100); assertResult(Lists.newArrayList(queue), 100);
@@ -176,12 +176,12 @@ public class IterableQueueTest
ExecutorService executor = Executors.newFixedThreadPool(threads); ExecutorService executor = Executors.newFixedThreadPool(threads);
List<Future<List<Integer>>> futures = Lists.newArrayList(); List<Future<List<Integer>>> futures = Lists.newArrayList();
final IterableQueue<Integer> queue = new IterableQueue<Integer>(); final IterableQueue<Integer> queue = new IterableQueue<>();
for (int i = 0; i < consumer; i++) for (int i = 0; i < consumer; i++)
{ {
Future<List<Integer>> future = Future<List<Integer>> future =
executor.submit(new CallableQueueCollector<Integer>(queue)); executor.submit(new CallableQueueCollector<>(queue));
futures.add(future); futures.add(future);
} }

View File

@@ -71,8 +71,8 @@ public class ScmModuleTest
assertThat( assertThat(
module.getExtensions(), module.getExtensions(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) String.class, String.class,
(Class<?>) Integer.class Integer.class
) )
); );
assertThat( assertThat(
@@ -86,8 +86,8 @@ public class ScmModuleTest
assertThat( assertThat(
module.getEvents(), module.getEvents(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) String.class, String.class,
(Class<?>) Boolean.class Boolean.class
) )
); );
assertThat( assertThat(
@@ -100,15 +100,15 @@ public class ScmModuleTest
assertThat( assertThat(
module.getRestProviders(), module.getRestProviders(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) Integer.class, Integer.class,
(Class<?>) Long.class Long.class
) )
); );
assertThat( assertThat(
module.getRestResources(), module.getRestResources(),
containsInAnyOrder( containsInAnyOrder(
(Class<?>) Float.class, Float.class,
(Class<?>) Double.class Double.class
) )
); );
//J+ //J+

View File

@@ -128,7 +128,7 @@ public class TemplateEngineFactoryTest
assertTrue(engines.contains(engine1)); assertTrue(engines.contains(engine1));
assertTrue(engines.contains(engine2)); assertTrue(engines.contains(engine2));
Set<TemplateEngine> ce = new HashSet<TemplateEngine>(); Set<TemplateEngine> ce = new HashSet<>();
ce.add(engine1); ce.add(engine1);
factory = new TemplateEngineFactory(ce, engine2); factory = new TemplateEngineFactory(ce, engine2);

View File

@@ -56,7 +56,7 @@ public class UrlBuilderTest
UrlBuilder builder = new UrlBuilder("http://www.short.de"); UrlBuilder builder = new UrlBuilder("http://www.short.de");
builder.appendParameter("i", 123).appendParameter("s", "abc"); builder.appendParameter("i", 123).appendParameter("s", "abc");
builder.appendParameter("b", true).appendParameter("l", 321l); builder.appendParameter("b", true).appendParameter("l", 321L);
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", builder.toString()); assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321", builder.toString());
builder.appendParameter("c", "a b"); builder.appendParameter("c", "a b");
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString()); assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString());

View File

@@ -199,7 +199,7 @@ public class XmlGroupDatabase implements XmlDatabase<Group>
/** Field description */ /** Field description */
@XmlJavaTypeAdapter(XmlGroupMapAdapter.class) @XmlJavaTypeAdapter(XmlGroupMapAdapter.class)
@XmlElement(name = "groups") @XmlElement(name = "groups")
private Map<String, Group> groupMap = new LinkedHashMap<String, Group>(); private Map<String, Group> groupMap = new LinkedHashMap<>();
/** Field description */ /** Field description */
private Long lastModified; private Long lastModified;

View File

@@ -72,7 +72,7 @@ public class XmlGroupList implements Iterable<Group>
*/ */
public XmlGroupList(Map<String, Group> groupMap) public XmlGroupList(Map<String, Group> groupMap)
{ {
this.groups = new LinkedList<Group>(groupMap.values()); this.groups = new LinkedList<>(groupMap.values());
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -81,7 +81,7 @@ public class XmlGroupMapAdapter
@Override @Override
public Map<String, Group> unmarshal(XmlGroupList groups) throws Exception public Map<String, Group> unmarshal(XmlGroupList groups) throws Exception
{ {
Map<String, Group> groupMap = new LinkedHashMap<String, Group>(); Map<String, Group> groupMap = new LinkedHashMap<>();
for (Group group : groups) for (Group group : groups)
{ {

View File

@@ -202,5 +202,5 @@ public class XmlUserDatabase implements XmlDatabase<User>
/** Field description */ /** Field description */
@XmlJavaTypeAdapter(XmlUserMapAdapter.class) @XmlJavaTypeAdapter(XmlUserMapAdapter.class)
@XmlElement(name = "users") @XmlElement(name = "users")
private Map<String, User> userMap = new LinkedHashMap<String, User>(); private Map<String, User> userMap = new LinkedHashMap<>();
} }

View File

@@ -72,7 +72,7 @@ public class XmlUserList implements Iterable<User>
*/ */
public XmlUserList(Map<String, User> userMap) public XmlUserList(Map<String, User> userMap)
{ {
this.users = new LinkedList<User>(userMap.values()); this.users = new LinkedList<>(userMap.values());
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -81,7 +81,7 @@ public class XmlUserMapAdapter
@Override @Override
public Map<String, User> unmarshal(XmlUserList users) throws Exception public Map<String, User> unmarshal(XmlUserList users) throws Exception
{ {
Map<String, User> userMap = new LinkedHashMap<String, User>(); Map<String, User> userMap = new LinkedHashMap<>();
for (User user : users) for (User user : users)
{ {

View File

@@ -52,7 +52,9 @@ public final class XmlStreams {
} }
private static XMLStreamReader createReader(Reader reader) throws XMLStreamException { private static XMLStreamReader createReader(Reader reader) throws XMLStreamException {
return XMLInputFactory.newInstance().createXMLStreamReader(reader); XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
return factory.createXMLStreamReader(reader);
} }

View File

@@ -1,6 +1,7 @@
package sonia.scm.it; package sonia.scm.it;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.assertj.core.api.AbstractCharSequenceAssert;
import org.assertj.core.util.Lists; import org.assertj.core.util.Lists;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
@@ -28,6 +29,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD; import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME; import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
@@ -94,8 +96,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse); String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff); String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, expected);
.isEqualTo(expected);
} }
@Test @Test
@@ -107,8 +108,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.removeAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt"), gitRepositoryResponse); String gitDiff = getDiff(RepositoryUtil.removeAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff); String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, expected);
.isEqualTo(expected);
} }
@Test @Test
@@ -120,8 +120,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), gitRepositoryResponse); String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "the updated content of a"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff); String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, expected);
.isEqualTo(expected);
} }
@Test @Test
@@ -161,21 +160,17 @@ public class DiffITCase {
String fileContent = getFileContent("/diff/largefile/original/SvnDiffGenerator_forTest"); String fileContent = getFileContent("/diff/largefile/original/SvnDiffGenerator_forTest");
String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse); String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse); String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
fileContent = getFileContent("/diff/largefile/modified/v1/SvnDiffGenerator_forTest"); fileContent = getFileContent("/diff/largefile/modified/v1/SvnDiffGenerator_forTest");
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse); svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse); gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
fileContent = getFileContent("/diff/largefile/modified/v2/SvnDiffGenerator_forTest"); fileContent = getFileContent("/diff/largefile/modified/v2/SvnDiffGenerator_forTest");
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse); svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse); gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
} }
/** /**
@@ -196,8 +191,7 @@ public class DiffITCase {
Changeset commit1 = RepositoryUtil.addFileAndCommit(gitRepositoryClient, fileName, ADMIN_USERNAME, ""); Changeset commit1 = RepositoryUtil.addFileAndCommit(gitRepositoryClient, fileName, ADMIN_USERNAME, "");
String svnDiff = getDiff(commit, svnRepositoryResponse); String svnDiff = getDiff(commit, svnRepositoryResponse);
String gitDiff = getDiff(commit1, gitRepositoryResponse); String gitDiff = getDiff(commit1, gitRepositoryResponse);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
} }
@@ -218,8 +212,7 @@ public class DiffITCase {
String gitDiff = getDiff(RepositoryUtil.addFileAndCommit(gitRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), gitRepositoryResponse); String gitDiff = getDiff(RepositoryUtil.addFileAndCommit(gitRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), gitRepositoryResponse);
String expected = getGitDiffWithoutIndexLine(gitDiff); String expected = getGitDiffWithoutIndexLine(gitDiff);
assertThat(svnDiff) assertDiffsAreEqual(svnDiff, expected);
.isEqualTo(expected);
} }
public String getFileContent(String name) throws URISyntaxException, IOException { public String getFileContent(String name) throws URISyntaxException, IOException {
@@ -242,6 +235,12 @@ public class DiffITCase {
return gitDiff.replaceAll(".*(index.*\n)", ""); return gitDiff.replaceAll(".*(index.*\n)", "");
} }
private void assertDiffsAreEqual(String svnDiff, String gitDiff) {
assertThat(svnDiff)
.as("diffs are different\n\nsvn:\n==================================================\n\n%s\n\ngit:\n==================================================\n\n%s)", svnDiff, gitDiff)
.isEqualTo(gitDiff);
}
private String getDiff(Changeset svnChangeset, ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> svnRepositoryResponse) { private String getDiff(Changeset svnChangeset, ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> svnRepositoryResponse) {
return svnRepositoryResponse.requestChangesets() return svnRepositoryResponse.requestChangesets()
.requestDiffInGitFormat(svnChangeset.getId()) .requestDiffInGitFormat(svnChangeset.getId())

View File

@@ -213,8 +213,14 @@ public class GitBrowseCommand extends AbstractGitCommand
if (lfsPointer.isPresent()) { if (lfsPointer.isPresent()) {
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
Blob blob = lfsBlobStore.get(lfsPointer.get().getOid().getName()); String oid = lfsPointer.get().getOid().getName();
Blob blob = lfsBlobStore.get(oid);
if (blob == null) {
logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName());
file.setLength(-1);
} else {
file.setLength(blob.getSize()); file.setLength(blob.getSize());
}
} else { } else {
file.setLength(loader.getSize()); file.setLength(loader.getSize());
} }

View File

@@ -145,7 +145,12 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException { private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException {
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository); BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
Blob blob = lfsBlobStore.get(lfsPointer.getOid().getName()); String oid = lfsPointer.getOid().getName();
Blob blob = lfsBlobStore.get(oid);
if (blob == null) {
logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName());
throw notFound(entity("LFS", oid).in(repository));
}
GitUtil.release(revWalk); GitUtil.release(revWalk);
GitUtil.release(treeWalk); GitUtil.release(treeWalk);
return new BlobLoader(blob); return new BlobLoader(blob);

View File

@@ -33,13 +33,18 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.util.QuotedString;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.DiffCommandBuilder; import sonia.scm.repository.api.DiffCommandBuilder;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
/** /**
*
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
public class GitDiffCommand extends AbstractGitCommand implements DiffCommand { public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
@@ -56,7 +61,7 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
Differ.Diff diff = Differ.diff(repository, request); Differ.Diff diff = Differ.diff(repository, request);
return output -> { return output -> {
try (DiffFormatter formatter = new DiffFormatter(output)) { try (DiffFormatter formatter = new DiffFormatter(new DequoteOutputStream(output))) {
formatter.setRepository(repository); formatter.setRepository(repository);
for (DiffEntry e : diff.getEntries()) { for (DiffEntry e : diff.getEntries()) {
@@ -70,4 +75,116 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
}; };
} }
static class DequoteOutputStream extends OutputStream {
private static final String[] DEQUOTE_STARTS = {
"--- ",
"+++ ",
"diff --git "
};
private final OutputStream target;
private boolean afterNL = true;
private boolean writeToBuffer = false;
private int numberOfPotentialBeginning = -1;
private int potentialBeginningCharCount = 0;
private boolean inPotentialQuotedLine = false;
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
DequoteOutputStream(OutputStream target) {
this.target = new BufferedOutputStream(target);
}
@Override
public void write(int i) throws IOException {
if (i == (int) '\n') {
handleNewLine(i);
return;
}
if (afterNL) {
afterNL = false;
if (foundPotentialBeginning(i)) {
return;
}
numberOfPotentialBeginning = -1;
inPotentialQuotedLine = false;
}
if (inPotentialQuotedLine && i == '"') {
handleQuote();
return;
}
if (numberOfPotentialBeginning > -1 && checkForFurtherBeginning(i)) {
return;
}
if (writeToBuffer) {
buffer.write(i);
} else {
target.write(i);
}
}
private boolean checkForFurtherBeginning(int i) throws IOException {
if (i == DEQUOTE_STARTS[numberOfPotentialBeginning].charAt(potentialBeginningCharCount)) {
if (potentialBeginningCharCount + 1 < DEQUOTE_STARTS[numberOfPotentialBeginning].length()) {
++potentialBeginningCharCount;
} else {
inPotentialQuotedLine = true;
}
target.write(i);
return true;
} else {
numberOfPotentialBeginning = -1;
}
return false;
}
private boolean foundPotentialBeginning(int i) throws IOException {
for (int n = 0; n < DEQUOTE_STARTS.length; ++n) {
if (i == DEQUOTE_STARTS[n].charAt(0)) {
numberOfPotentialBeginning = n;
potentialBeginningCharCount = 1;
target.write(i);
return true;
}
}
return false;
}
private void handleQuote() throws IOException {
if (writeToBuffer) {
buffer.write('"');
dequoteBuffer();
} else {
writeToBuffer = true;
buffer.reset();
buffer.write('"');
}
}
private void handleNewLine(int i) throws IOException {
afterNL = true;
if (writeToBuffer) {
dequoteBuffer();
}
target.write(i);
}
private void dequoteBuffer() throws IOException {
byte[] bytes = buffer.toByteArray();
String dequote = QuotedString.GIT_PATH.dequote(bytes, 0, bytes.length);
target.write(dequote.getBytes(UTF_8));
writeToBuffer = false;
}
@Override
public void flush() throws IOException {
target.flush();
}
}
} }

View File

@@ -14,6 +14,12 @@ const Switcher = styled(ButtonAddons)`
right: 0; right: 0;
`; `;
const SmallButton = styled(Button).attrs(props => ({
className: "is-small"
}))`
height: inherit;
`;
type Props = { type Props = {
repository: Repository; repository: Repository;
}; };
@@ -62,9 +68,9 @@ export default class ProtocolInformation extends React.Component<Props, State> {
} }
return ( return (
<Button color={color} action={() => this.selectProtocol(protocol)}> <SmallButton color={color} action={() => this.selectProtocol(protocol)}>
{name.toUpperCase()} {name.toUpperCase()}
</Button> </SmallButton>
); );
}; };

View File

@@ -0,0 +1,35 @@
package sonia.scm.repository.spi;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class GitDiffCommand_DequoteOutputStreamTest {
@Test
void shouldDequoteText() throws IOException {
String s = "diff --git \"a/file \\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 a\" \"b/file \\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 b\"\n" +
"new file mode 100644\n" +
"index 0000000..8cb0607\n" +
"--- /dev/null\n" +
"+++ \"b/\\303\\272\\303\\274\\303\\276\\303\\253\\303\\251\\303\\245\\303\\253\\303\\245\\303\\251 \\303\\245g\\303\\260f\\303\\237\"\n" +
"@@ -0,0 +1 @@\n" +
"+String s = \"quotes shall be kept\";";
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
GitDiffCommand.DequoteOutputStream stream = new GitDiffCommand.DequoteOutputStream(buffer);
byte[] bytes = s.getBytes();
stream.write(bytes, 0, bytes.length);
stream.flush();
Assertions.assertThat(buffer.toString()).isEqualTo("diff --git a/file úüþëéåëåé a b/file úüþëéåëåé b\n" +
"new file mode 100644\n" +
"index 0000000..8cb0607\n" +
"--- /dev/null\n" +
"+++ b/úüþëéåëåé ågðfß\n" +
"@@ -0,0 +1 @@\n" +
"+String s = \"quotes shall be kept\";");
}
}

View File

@@ -145,7 +145,7 @@ public class HgPackageReader
*/ */
private void filterPackage(HgPackages packages) private void filterPackage(HgPackages packages)
{ {
List<HgPackage> pkgList = new ArrayList<HgPackage>(); List<HgPackage> pkgList = new ArrayList<>();
for (HgPackage pkg : packages) for (HgPackage pkg : packages)
{ {
@@ -228,7 +228,7 @@ public class HgPackageReader
if (packages == null) if (packages == null)
{ {
packages = new HgPackages(); packages = new HgPackages();
packages.setPackages(new ArrayList<HgPackage>()); packages.setPackages(new ArrayList<>());
} }
return packages; return packages;

View File

@@ -77,7 +77,9 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
String revision = MoreObjects.firstNonNull(request.getRevision(), "tip"); String revision = MoreObjects.firstNonNull(request.getRevision(), "tip");
Changeset c = LogCommand.on(getContext().open()).rev(revision).limit(1).single(); Changeset c = LogCommand.on(getContext().open()).rev(revision).limit(1).single();
if (c != null) {
cmd.rev(c.getNode()); cmd.rev(c.getNode());
}
if (!Strings.isNullOrEmpty(request.getPath())) if (!Strings.isNullOrEmpty(request.getPath()))
{ {
@@ -100,6 +102,6 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
} }
FileObject file = cmd.execute(); FileObject file = cmd.execute();
return new BrowserResult(c.getNode(), revision, file); return new BrowserResult(c == null? "tip": c.getNode(), revision, file);
} }
} }

View File

@@ -211,15 +211,7 @@ public class SVNKitLogger extends SVNDebugLogAdapter
*/ */
private Logger getLogger(SVNLogType type) private Logger getLogger(SVNLogType type)
{ {
Logger logger = loggerMap.get(type); return loggerMap.computeIfAbsent(type, t -> LoggerFactory.getLogger(parseName(t.getName())));
if (logger == null)
{
logger = LoggerFactory.getLogger(parseName(type.getName()));
loggerMap.put(type, logger);
}
return logger;
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------

View File

@@ -81,15 +81,7 @@ public class MapCacheManager
@Override @Override
public synchronized <K, V> MapCache<K, V> getCache(String name) public synchronized <K, V> MapCache<K, V> getCache(String name)
{ {
MapCache<K, V> cache = cacheMap.get(name); return (MapCache<K, V>) cacheMap.computeIfAbsent(name, k -> new MapCache<K, V>());
if (cache == null)
{
cache = new MapCache<K, V>();
cacheMap.put(name, cache);
}
return cache;
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------

View File

@@ -17,6 +17,7 @@
<name>scm-ui</name> <name>scm-ui</name>
<properties> <properties>
<build.script>build</build.script>
<sonar.language>typescript</sonar.language> <sonar.language>typescript</sonar.language>
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources> <sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions> <sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
@@ -68,7 +69,7 @@
<goal>run</goal> <goal>run</goal>
</goals> </goals>
<configuration> <configuration>
<script>build</script> <script>${build.script}</script>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@@ -118,4 +119,21 @@
</plugins> </plugins>
</build> </build>
<profiles>
<profile>
<id>dev</id>
<activation>
<property>
<name>development</name>
</property>
</activation>
<properties>
<build.script>build:dev</build.script>
</properties>
</profile>
</profiles>
</project> </project>

View File

@@ -7,7 +7,7 @@
"private": false, "private": false,
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"prettier": "^1.18.2" "prettier": "^1.19.1"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -7,7 +7,7 @@
"private": false, "private": false,
"main": "tsconfig.json", "main": "tsconfig.json",
"dependencies": { "dependencies": {
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -42,7 +42,7 @@
"raf": "^3.4.0", "raf": "^3.4.0",
"react-test-renderer": "^16.10.2", "react-test-renderer": "^16.10.2",
"storybook-addon-i18next": "^1.2.1", "storybook-addon-i18next": "^1.2.1",
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"dependencies": { "dependencies": {
"@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT", "@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT",

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import classNames from "classnames";
import { Async, AsyncCreatable } from "react-select"; import { Async, AsyncCreatable } from "react-select";
import { SelectValue } from "@scm-manager/ui-types"; import { SelectValue } from "@scm-manager/ui-types";
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
@@ -7,13 +8,14 @@ import { ActionMeta, ValueType } from "react-select/lib/types";
type Props = { type Props = {
loadSuggestions: (p: string) => Promise<SelectValue[]>; loadSuggestions: (p: string) => Promise<SelectValue[]>;
valueSelected: (p: SelectValue) => void; valueSelected: (p: SelectValue) => void;
label: string; label?: string;
helpText?: string; helpText?: string;
value?: SelectValue; value?: SelectValue;
placeholder: string; placeholder: string;
loadingMessage: string; loadingMessage: string;
noOptionsMessage: string; noOptionsMessage: string;
creatable?: boolean; creatable?: boolean;
className?: string;
}; };
type State = {}; type State = {};
@@ -53,10 +55,11 @@ class Autocomplete extends React.Component<Props, State> {
loadingMessage, loadingMessage,
noOptionsMessage, noOptionsMessage,
loadSuggestions, loadSuggestions,
creatable creatable,
className
} = this.props; } = this.props;
return ( return (
<div className="field"> <div className={classNames("field", className)}>
<LabelWithHelpIcon label={label} helpText={helpText} /> <LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control"> <div className="control">
{creatable ? ( {creatable ? (

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types"; import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types";
import Autocomplete from "./Autocomplete"; import Autocomplete from "./Autocomplete";
import { apiClient } from "./apiclient";
export type AutocompleteProps = { export type AutocompleteProps = {
autocompleteLink?: string; autocompleteLink?: string;
@@ -19,7 +20,8 @@ export default class UserGroupAutocomplete extends React.Component<Props> {
loadSuggestions = (inputValue: string): Promise<SelectValue[]> => { loadSuggestions = (inputValue: string): Promise<SelectValue[]> => {
const url = this.props.autocompleteLink; const url = this.props.autocompleteLink;
const link = url + "?q="; const link = url + "?q=";
return fetch(link + inputValue) return apiClient
.get(link + inputValue)
.then(response => response.json()) .then(response => response.json())
.then((json: AutocompleteObject[]) => { .then((json: AutocompleteObject[]) => {
return json.map(element => { return json.map(element => {

View File

@@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
exports[`Storyshots Forms|Checkbox Default 1`] = ` exports[`Storyshots Forms|Checkbox Default 1`] = `
<div <div
className="sc-gipzik xsalO" className="sc-fBuWsC ldmpJA"
> >
<div <div
className="field" className="field"
@@ -381,7 +381,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
exports[`Storyshots Forms|Checkbox Disabled 1`] = ` exports[`Storyshots Forms|Checkbox Disabled 1`] = `
<div <div
className="sc-gipzik xsalO" className="sc-fBuWsC ldmpJA"
> >
<div <div
className="field" className="field"
@@ -409,7 +409,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
exports[`Storyshots Forms|Radio Default 1`] = ` exports[`Storyshots Forms|Radio Default 1`] = `
<div <div
className="sc-csuQGl fFFkRK" className="sc-fMiknA keSQNk"
> >
<label <label
className="radio" className="radio"
@@ -438,7 +438,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
exports[`Storyshots Forms|Radio Disabled 1`] = ` exports[`Storyshots Forms|Radio Disabled 1`] = `
<div <div
className="sc-csuQGl fFFkRK" className="sc-fMiknA keSQNk"
> >
<label <label
className="radio" className="radio"
@@ -2311,3 +2311,173 @@ PORT_NUMBER =
</pre> </pre>
</div> </div>
`; `;
exports[`Storyshots Table|Table Default 1`] = `
<table
className="sc-jhAzac hmXDXQ table content is-hoverable"
>
<thead>
<tr>
<th
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
First Name
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Last Name
<i
className="fas fa-sort-amount-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
E-Mail
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h4>
Tricia
</h4>
</td>
<td>
<b
style={
Object {
"color": "red",
}
}
>
McMillan
</b>
</td>
<td>
<a>
tricia@hitchhiker.com
</a>
</td>
</tr>
<tr>
<td>
<h4>
Arthur
</h4>
</td>
<td>
<b
style={
Object {
"color": "red",
}
}
>
Dent
</b>
</td>
<td>
<a>
arthur@hitchhiker.com
</a>
</td>
</tr>
</tbody>
</table>
`;
exports[`Storyshots Table|Table Empty 1`] = `
<div
className="notification is-info"
>
No data found.
</div>
`;
exports[`Storyshots Table|Table TextColumn 1`] = `
<table
className="sc-jhAzac hmXDXQ table content is-hoverable"
>
<thead>
<tr>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Id
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Name
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
<th
className="has-cursor-pointer"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Description
<i
className="fas fa-sort-alpha-down has-text-grey-light sc-hzDkRC escBde"
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
21
</td>
<td>
Pommes
</td>
<td>
Fried potato sticks
</td>
</tr>
<tr>
<td>
42
</td>
<td>
Quarter-Pounder
</td>
<td>
Big burger
</td>
</tr>
<tr>
<td>
-84
</td>
<td>
Icecream
</td>
<td>
Cold dessert
</td>
</tr>
</tbody>
</table>
`;

View File

@@ -1,4 +1,4 @@
import { apiClient, createUrl } from "./apiclient"; import { apiClient, createUrl, extractXsrfTokenFromCookie } from "./apiclient";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import { BackendError } from "./errors"; import { BackendError } from "./errors";
@@ -70,3 +70,22 @@ describe("error handling tests", () => {
}); });
}); });
}); });
describe("extract xsrf token", () => {
it("should return undefined if no cookie exists", () => {
const token = extractXsrfTokenFromCookie(undefined);
expect(token).toBeUndefined();
});
it("should return undefined without X-Bearer-Token exists", () => {
const token = extractXsrfTokenFromCookie("a=b; c=d; e=f");
expect(token).toBeUndefined();
});
it("should return xsrf token", () => {
const cookie =
"a=b; X-Bearer-Token=eyJhbGciOiJIUzI1NiJ9.eyJ4c3JmIjoiYjE0NDRmNWEtOWI5Mi00ZDA0LWFkMzMtMTAxYjY3MWQ1YTc0Iiwic3ViIjoic2NtYWRtaW4iLCJqdGkiOiI2RFJpQVphNWwxIiwiaWF0IjoxNTc0MDcyNDQ4LCJleHAiOjE1NzQwNzYwNDgsInNjbS1tYW5hZ2VyLnJlZnJlc2hFeHBpcmF0aW9uIjoxNTc0MTE1NjQ4OTU5LCJzY20tbWFuYWdlci5wYXJlbnRUb2tlbklkIjoiNkRSaUFaYTVsMSJ9.VUJtKeWUn3xtHCEbG51r7ceXZ8CF3cmN8J-eb9EDY_U; c=d";
const token = extractXsrfTokenFromCookie(cookie);
expect(token).toBe("b1444f5a-9b92-4d04-ad33-101b671d5a74");
});
});

View File

@@ -9,14 +9,52 @@ const sessionId = (
.substr(2, 5) .substr(2, 5)
).toUpperCase(); ).toUpperCase();
const applyFetchOptions: (p: RequestInit) => RequestInit = o => { const extractXsrfTokenFromJwt = (jwt: string) => {
o.credentials = "same-origin"; const parts = jwt.split(".");
o.headers = { if (parts.length === 3) {
Cache: "no-cache", return JSON.parse(atob(parts[1])).xsrf;
// identify the request as ajax request }
"X-Requested-With": "XMLHttpRequest",
"X-SCM-Session-ID": sessionId
}; };
// @VisibleForTesting
export const extractXsrfTokenFromCookie = (cookieString?: string) => {
if (cookieString) {
const cookies = cookieString.split(";");
for (const c of cookies) {
const parts = c.trim().split("=");
if (parts[0] === "X-Bearer-Token") {
return extractXsrfTokenFromJwt(parts[1]);
}
}
}
};
const extractXsrfToken = () => {
return extractXsrfTokenFromCookie(document.cookie);
};
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
if (!o.headers) {
o.headers = {};
}
// @ts-ignore We are sure that here we only get headers of type Record<string, string>
const headers: Record<string, string> = o.headers;
headers["Cache"] = "no-cache";
// identify the request as ajax request
headers["X-Requested-With"] = "XMLHttpRequest";
// identify the web interface
headers["X-SCM-Client"] = "WUI";
// identify the window session
headers["X-SCM-Session-ID"] = sessionId
const xsrf = extractXsrfToken();
if (xsrf) {
headers["X-XSRF-Token"] = xsrf;
}
o.credentials = "same-origin";
o.headers = headers;
return o; return o;
}; };
@@ -55,23 +93,32 @@ class ApiClient {
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure); return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
} }
post(url: string, payload: any, contentType = "application/json") { post(url: string, payload?: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithJSONBody("POST", url, contentType, payload); return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload);
} }
postBinary(url: string, fileAppender: (p: FormData) => void) { postText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithTextBody("POST", url, additionalHeaders, payload);
}
putText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithTextBody("PUT", url, additionalHeaders, payload);
}
postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders: Record<string, string> = {}) {
const formData = new FormData(); const formData = new FormData();
fileAppender(formData); fileAppender(formData);
const options: RequestInit = { const options: RequestInit = {
method: "POST", method: "POST",
body: formData body: formData,
headers: additionalHeaders
}; };
return this.httpRequestWithBinaryBody(options, url); return this.httpRequestWithBinaryBody(options, url);
} }
put(url: string, payload: any, contentType = "application/json") { put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
return this.httpRequestWithJSONBody("PUT", url, contentType, payload); return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
} }
head(url: string) { head(url: string) {
@@ -90,21 +137,44 @@ class ApiClient {
return fetch(createUrl(url), options).then(handleFailure); return fetch(createUrl(url), options).then(handleFailure);
} }
httpRequestWithJSONBody(method: string, url: string, contentType: string, payload: any): Promise<Response> { httpRequestWithJSONBody(
method: string,
url: string,
contentType: string,
additionalHeaders: Record<string, string>,
payload?: any
): Promise<Response> {
const options: RequestInit = { const options: RequestInit = {
method: method, method: method,
body: JSON.stringify(payload) headers: additionalHeaders
}; };
if (payload) {
options.body = JSON.stringify(payload);
}
return this.httpRequestWithBinaryBody(options, url, contentType); return this.httpRequestWithBinaryBody(options, url, contentType);
} }
httpRequestWithTextBody(
method: string,
url: string,
additionalHeaders: Record<string, string> = {},
payload: string
) {
const options: RequestInit = {
method: method,
headers: additionalHeaders
};
options.body = payload;
return this.httpRequestWithBinaryBody(options, url, "text/plain");
}
httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) { httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) {
options = applyFetchOptions(options); options = applyFetchOptions(options);
if (contentType) { if (contentType) {
if (!options.headers) { if (!options.headers) {
options.headers = new Headers(); options.headers = {};
} }
// @ts-ignore // @ts-ignore We are sure that here we only get headers of type Record<string, string>
options.headers["Content-Type"] = contentType; options.headers["Content-Type"] = contentType;
} }

View File

@@ -18,6 +18,7 @@ export type ButtonProps = {
type Props = ButtonProps & type Props = ButtonProps &
RouteComponentProps & { RouteComponentProps & {
title?: string;
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
color?: string; color?: string;
}; };
@@ -38,7 +39,19 @@ class Button extends React.Component<Props> {
}; };
render() { render() {
const { label, loading, disabled, type, color, className, icon, fullWidth, reducedMobile, children } = this.props; const {
label,
title,
loading,
disabled,
type,
color,
className,
icon,
fullWidth,
reducedMobile,
children
} = this.props;
const loadingClass = loading ? "is-loading" : ""; const loadingClass = loading ? "is-loading" : "";
const fullWidthClass = fullWidth ? "is-fullwidth" : ""; const fullWidthClass = fullWidth ? "is-fullwidth" : "";
const reducedMobileClass = reducedMobile ? "is-reduced-mobile" : ""; const reducedMobileClass = reducedMobile ? "is-reduced-mobile" : "";
@@ -46,6 +59,7 @@ class Button extends React.Component<Props> {
return ( return (
<button <button
type={type} type={type}
title={title}
disabled={disabled} disabled={disabled}
onClick={this.onClick} onClick={this.onClick}
className={classNames("button", "is-" + color, loadingClass, fullWidthClass, reducedMobileClass, className)} className={classNames("button", "is-" + color, loadingClass, fullWidthClass, reducedMobileClass, className)}
@@ -63,6 +77,7 @@ class Button extends React.Component<Props> {
return ( return (
<button <button
type={type} type={type}
title={title}
disabled={disabled} disabled={disabled}
onClick={this.onClick} onClick={this.onClick}
className={classNames("button", "is-" + color, loadingClass, fullWidthClass, className)} className={classNames("button", "is-" + color, loadingClass, fullWidthClass, className)}

View File

@@ -2,8 +2,8 @@ import React from "react";
type Props = { type Props = {
displayName: string; displayName: string;
url: string; url?: string;
disabled: boolean; disabled?: boolean;
onClick?: () => void; onClick?: () => void;
}; };

View File

@@ -48,7 +48,6 @@ const buttonStory = (name: string, storyFn: () => ReactElement) => {
.addDecorator(SpacingDecorator) .addDecorator(SpacingDecorator)
.add("Default", storyFn); .add("Default", storyFn);
}; };
buttonStory("AddButton", () => <AddButton>Add</AddButton>); buttonStory("AddButton", () => <AddButton>Add</AddButton>);
buttonStory("CreateButton", () => <CreateButton>Create</CreateButton>); buttonStory("CreateButton", () => <CreateButton>Create</CreateButton>);
buttonStory("DeleteButton", () => <DeleteButton>Delete</DeleteButton>); buttonStory("DeleteButton", () => <DeleteButton>Delete</DeleteButton>);

View File

@@ -0,0 +1,126 @@
import { byKey, byValueLength, byNestedKeys } from "./comparators";
const createObject = (key: string, value?: string) => {
return {
[key]: value
};
};
const createObjects = (key: string, values: Array<string | undefined>) => {
return values.map(v => createObject(key, v));
};
describe("key comparator tests", () => {
it("should sort array", () => {
const array = createObjects("key", ["z", "a", "y", "b"]);
array.sort(byKey("key"));
expect(array).toEqual(createObjects("key", ["a", "b", "y", "z"]));
});
it("should not fail if value is undefined", () => {
const array = createObjects("key", ["z", undefined, "a"]);
array.sort(byKey("key"));
expect(array).toEqual(createObjects("key", ["a", "z", undefined]));
});
it("should not fail if key is undefined", () => {
const array = createObjects("key", ["a"]);
array.push({});
array.push(createObject("key", "z"));
array.sort(byKey("key"));
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), {}]);
});
it("should not fail if item is undefined", () => {
const array: any[] = createObjects("key", ["a"]);
array.push(undefined);
array.push(createObject("key", "z"));
array.sort(byKey("key"));
expect(array).toEqual([createObject("key", "a"), createObject("key", "z"), undefined]);
});
});
describe("length comparator tests", () => {
it("should sort array", () => {
const array = createObjects("key", ["....", ".", "...", ".."]);
array.sort(byValueLength("key"));
expect(array).toEqual(createObjects("key", [".", "..", "...", "...."]));
});
it("should not fail if value is undefined", () => {
const array = createObjects("key", ["..", undefined, "."]);
array.sort(byValueLength("key"));
expect(array).toEqual(createObjects("key", [".", "..", undefined]));
});
it("should not fail if key is undefined", () => {
const array = createObjects("key", ["."]);
array.push({});
array.push(createObject("key", ".."));
array.sort(byValueLength("key"));
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), {}]);
});
it("should not fail if item is undefined", () => {
const array: any[] = createObjects("key", ["."]);
array.push(undefined);
array.push(createObject("key", ".."));
array.sort(byValueLength("key"));
expect(array).toEqual([createObject("key", "."), createObject("key", ".."), undefined]);
});
});
describe("nested key comparator tests", () => {
const createObject = (key: string, nested?: string, value?: string) => {
if (!nested) {
return {
[key]: undefined
};
}
return {
[key]: {
[nested]: value
}
};
};
const createObjects = (key: string, nested: string, values: Array<string | undefined>) => {
return values.map(v => createObject(key, nested, v));
};
it("should sort array", () => {
const array = createObjects("key", "nested", ["z", "a", "y", "b"]);
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual(createObjects("key", "nested", ["a", "b", "y", "z"]));
});
it("should not fail if value is undefined", () => {
const array = createObjects("key", "nested", ["z", undefined, "a"]);
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual(createObjects("key", "nested", ["a", "z", undefined]));
});
it("should not fail if key is undefined", () => {
const array = createObjects("key", "nested", ["a"]);
array.push({});
array.push(createObject("key", "nested", "z"));
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), {}]);
});
it("should not fail if nested key is undefined", () => {
const array = createObjects("key", "nested", ["a"]);
array.push(createObject("key", undefined, "y"));
array.push(createObject("key", "nested", "z"));
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), { key: undefined }]);
});
it("should not fail if item is undefined", () => {
const array: any[] = createObjects("key", "nested", ["a"]);
array.push(undefined);
array.push(createObject("key", "nested", "z"));
array.sort(byNestedKeys("key", "nested"));
expect(array).toEqual([createObject("key", "nested", "a"), createObject("key", "nested", "z"), undefined]);
});
});

View File

@@ -0,0 +1,75 @@
const isUndefined = (o: any, key: string, nested?: string) => {
if (typeof o === "undefined" || typeof o[key] === "undefined") {
return true;
}
if (nested) {
return typeof o[key][nested] === "undefined";
}
return false;
};
export const byKey = (key: string) => {
return (a: any, b: any) => {
if (isUndefined(a, key)) {
return 1;
}
if (isUndefined(b, key)) {
return 0;
}
if (a[key] < b[key]) {
return -1;
} else if (a[key] > b[key]) {
return 1;
} else {
return 0;
}
};
};
export const byValueLength = (key: string) => {
return (a: any, b: any) => {
if (isUndefined(a, key)) {
return 1;
}
if (isUndefined(b, key)) {
return 0;
}
if (a[key].length < b[key].length) {
return -1;
} else if (a[key].length > b[key].length) {
return 1;
} else {
return 0;
}
};
};
export const byNestedKeys = (key: string, nestedKey: string) => {
return (a: any, b: any) => {
if (isUndefined(a, key, nestedKey)) {
return 1;
}
if (isUndefined(b, key, nestedKey)) {
return 0;
}
if (a[key][nestedKey] < b[key][nestedKey]) {
return -1;
} else if (a[key][nestedKey] > b[key][nestedKey]) {
return 1;
} else {
return 0;
}
};
};
export default {
byKey,
byValueLength,
byNestedKeys
};

View File

@@ -1,8 +1,7 @@
import React from "react"; import React, { FormEvent } from "react";
import { WithTranslation, withTranslation } from "react-i18next"; import { WithTranslation, withTranslation } from "react-i18next";
import { Links, Link } from "@scm-manager/ui-types"; import { Links, Link } from "@scm-manager/ui-types";
import { apiClient, SubmitButton, Loading, ErrorNotification } from "../"; import { apiClient, Level, SubmitButton, Loading, ErrorNotification } from "../";
import { FormEvent } from "react";
type RenderProps = { type RenderProps = {
readOnly: boolean; readOnly: boolean;
@@ -179,7 +178,9 @@ class Configuration extends React.Component<Props, State> {
<form onSubmit={this.modifyConfiguration}> <form onSubmit={this.modifyConfiguration}>
{this.props.render(renderProps)} {this.props.render(renderProps)}
<hr /> <hr />
<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} /> <Level
right={<SubmitButton label={t("config.form.submit")} disabled={!valid || readOnly} loading={modifying} />}
/>
</form> </form>
</> </>
); );

View File

@@ -1,7 +1,8 @@
import React, { MouseEvent } from "react"; import React, { MouseEvent } from "react";
import styled from "styled-components";
import { AddButton } from "../buttons"; import Level from "../layout/Level";
import InputField from "./InputField"; import InputField from "./InputField";
import AddButton from "../buttons/AddButton";
type Props = { type Props = {
addEntry: (p: string) => void; addEntry: (p: string) => void;
@@ -17,6 +18,22 @@ type State = {
entryToAdd: string; entryToAdd: string;
}; };
const StyledLevel = styled(Level)`
align-items: stretch;
margin-bottom: 1rem !important; // same margin as field
`;
const StyledInputField = styled(InputField)`
width: 100%;
margin-right: 1.5rem;
`;
const StyledField = styled.div.attrs(props => ({
className: "field"
}))`
align-self: flex-end;
`;
class AddEntryToTableField extends React.Component<Props, State> { class AddEntryToTableField extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -37,8 +54,9 @@ class AddEntryToTableField extends React.Component<Props, State> {
render() { render() {
const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props; const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props;
return ( return (
<> <StyledLevel
<InputField children={
<StyledInputField
label={fieldLabel} label={fieldLabel}
errorMessage={errorMessage} errorMessage={errorMessage}
onChange={this.handleAddEntryChange} onChange={this.handleAddEntryChange}
@@ -48,12 +66,17 @@ class AddEntryToTableField extends React.Component<Props, State> {
disabled={disabled} disabled={disabled}
helpText={helpText} helpText={helpText}
/> />
}
right={
<StyledField>
<AddButton <AddButton
label={buttonLabel} label={buttonLabel}
action={this.addButtonClicked} action={this.addButtonClicked}
disabled={disabled || this.state.entryToAdd === "" || !this.isValid()} disabled={disabled || this.state.entryToAdd === "" || !this.isValid()}
/> />
</> </StyledField>
}
/>
); );
} }

View File

@@ -1,6 +1,7 @@
import React, { MouseEvent } from "react"; import React, { MouseEvent } from "react";
import styled from "styled-components";
import { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; import { SelectValue } from "@scm-manager/ui-types";
import Level from "../layout/Level";
import Autocomplete from "../Autocomplete"; import Autocomplete from "../Autocomplete";
import AddButton from "../buttons/AddButton"; import AddButton from "../buttons/AddButton";
@@ -20,6 +21,11 @@ type State = {
selectedValue?: SelectValue; selectedValue?: SelectValue;
}; };
const StyledAutocomplete = styled(Autocomplete)`
width: 100%;
margin-right: 1.5rem;
`;
class AutocompleteAddEntryToTableField extends React.Component<Props, State> { class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -41,8 +47,9 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
const { selectedValue } = this.state; const { selectedValue } = this.state;
return ( return (
<div className="field"> <Level
<Autocomplete children={
<StyledAutocomplete
label={fieldLabel} label={fieldLabel}
loadSuggestions={loadSuggestions} loadSuggestions={loadSuggestions}
valueSelected={this.handleAddEntryChange} valueSelected={this.handleAddEntryChange}
@@ -53,9 +60,13 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
noOptionsMessage={noOptionsMessage} noOptionsMessage={noOptionsMessage}
creatable={true} creatable={true}
/> />
}
right={
<div className="field">
<AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} /> <AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} />
</div> </div>
}
/>
); );
} }

View File

@@ -15,6 +15,7 @@ type Props = {
errorMessage?: string; errorMessage?: string;
disabled?: boolean; disabled?: boolean;
helpText?: string; helpText?: string;
className?: string;
}; };
class InputField extends React.Component<Props> { class InputField extends React.Component<Props> {
@@ -47,11 +48,21 @@ class InputField extends React.Component<Props> {
}; };
render() { render() {
const { type, placeholder, value, validationError, errorMessage, disabled, label, helpText } = this.props; const {
type,
placeholder,
value,
validationError,
errorMessage,
disabled,
label,
helpText,
className
} = this.props;
const errorView = validationError ? "is-danger" : ""; const errorView = validationError ? "is-danger" : "";
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : ""; const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
return ( return (
<div className="field"> <div className={classNames("field", className)}>
<LabelWithHelpIcon label={label} helpText={helpText} /> <LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control"> <div className="control">
<input <input

View File

@@ -52,6 +52,8 @@ export { default as OverviewPageActions } from "./OverviewPageActions";
export { default as CardColumnGroup } from "./CardColumnGroup"; export { default as CardColumnGroup } from "./CardColumnGroup";
export { default as CardColumn } from "./CardColumn"; export { default as CardColumn } from "./CardColumn";
export { default as comparators } from "./comparators";
export { apiClient } from "./apiclient"; export { apiClient } from "./apiclient";
export * from "./errors"; export * from "./errors";
@@ -63,6 +65,7 @@ export * from "./layout";
export * from "./modals"; export * from "./modals";
export * from "./navigation"; export * from "./navigation";
export * from "./repos"; export * from "./repos";
export * from "./table";
export { export {
File, File,

View File

@@ -4,15 +4,22 @@ import classNames from "classnames";
type Props = { type Props = {
className?: string; className?: string;
left?: ReactNode; left?: ReactNode;
children?: ReactNode;
right?: ReactNode; right?: ReactNode;
}; };
export default class Level extends React.Component<Props> { export default class Level extends React.Component<Props> {
render() { render() {
const { className, left, right } = this.props; const { className, left, children, right } = this.props;
let child = null;
if (children) {
child = <div className="level-item">{children}</div>;
}
return ( return (
<div className={classNames("level", className)}> <div className={classNames("level", className)}>
<div className="level-left">{left}</div> <div className="level-left">{left}</div>
{child}
<div className="level-right">{right}</div> <div className="level-right">{right}</div>
</div> </div>
); );

View File

@@ -1,8 +1,11 @@
import React from "react"; import React from "react";
import DiffFile from "./DiffFile"; import DiffFile from "./DiffFile";
import { DiffObjectProps, File } from "./DiffTypes"; import { DiffObjectProps, File } from "./DiffTypes";
import Notification from "../Notification";
import { WithTranslation, withTranslation } from "react-i18next";
type Props = DiffObjectProps & { type Props = WithTranslation &
DiffObjectProps & {
diff: File[]; diff: File[];
defaultCollapse?: boolean; defaultCollapse?: boolean;
}; };
@@ -13,15 +16,17 @@ class Diff extends React.Component<Props> {
}; };
render() { render() {
const { diff, ...fileProps } = this.props; const { diff, t, ...fileProps } = this.props;
return ( return (
<> <>
{diff.map((file, index) => ( {diff.length === 0 ? (
<DiffFile key={index} file={file} {...fileProps} {...this.props} /> <Notification type="info">{t("diff.noDiffFound")}</Notification>
))} ) : (
diff.map((file, index) => <DiffFile key={index} file={file} {...fileProps} {...this.props} />)
)}
</> </>
); );
} }
} }
export default Diff; export default withTranslation("repos")(Diff);

View File

@@ -43,6 +43,7 @@ class LoadingDiff extends React.Component<Props, State> {
fetchDiff = () => { fetchDiff = () => {
const { url } = this.props; const { url } = this.props;
this.setState({loading: true});
apiClient apiClient
.get(url) .get(url)
.then(response => response.text()) .then(response => response.text())

View File

@@ -0,0 +1,18 @@
import React, { FC, ReactNode } from "react";
import { ColumnProps } from "./types";
type Props = ColumnProps & {
children: (row: any, columnIndex: number) => ReactNode;
};
const Column: FC<Props> = ({ row, columnIndex, children }) => {
if (row === undefined) {
throw new Error("missing row, use column only as child of Table");
}
if (columnIndex === undefined) {
throw new Error("missing row, use column only as child of Table");
}
return <>{children(row, columnIndex)}</>;
};
export default Column;

View File

@@ -0,0 +1,19 @@
import React, { FC } from "react";
import styled from "styled-components";
import Icon from "../Icon";
type Props = {
name: string;
isVisible: boolean;
};
const IconWithMarginLeft = styled(Icon)`
visibility: ${(props: Props) => (props.isVisible ? "visible" : "hidden")};
margin-left: 0.25em;
`;
const SortIcon: FC<Props> = (props: Props) => {
return <IconWithMarginLeft name={props.name} isVisible={props.isVisible} />;
};
export default SortIcon;

View File

@@ -0,0 +1,53 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import Table from "./Table";
import Column from "./Column";
import TextColumn from "./TextColumn";
storiesOf("Table|Table", module)
.add("Default", () => (
<Table
data={[
{ firstname: "Tricia", lastname: "McMillan", email: "tricia@hitchhiker.com" },
{ firstname: "Arthur", lastname: "Dent", email: "arthur@hitchhiker.com" }
]}
>
<Column header={"First Name"}>{(row: any) => <h4>{row.firstname}</h4>}</Column>
<Column
header={"Last Name"}
createComparator={() => {
return (a: any, b: any) => {
if (a.lastname > b.lastname) {
return -1;
} else if (a.lastname < b.lastname) {
return 1;
} else {
return 0;
}
};
}}
>
{(row: any) => <b style={{ color: "red" }}>{row.lastname}</b>}
</Column>
<Column header={"E-Mail"}>{(row: any) => <a>{row.email}</a>}</Column>
</Table>
))
.add("TextColumn", () => (
<Table
data={[
{ id: "21", title: "Pommes", desc: "Fried potato sticks" },
{ id: "42", title: "Quarter-Pounder", desc: "Big burger" },
{ id: "-84", title: "Icecream", desc: "Cold dessert" }
]}
>
<TextColumn header="Id" dataKey="id" />
<TextColumn header="Name" dataKey="title" />
<TextColumn header="Description" dataKey="desc" />
</Table>
))
.add("Empty", () => (
<Table data={[]} emptyMessage="No data found.">
<TextColumn header="Id" dataKey="id" />
<TextColumn header="Name" dataKey="name" />
</Table>
));

View File

@@ -0,0 +1,120 @@
import React, { FC, ReactElement, useEffect, useState } from "react";
import styled from "styled-components";
import { Comparator } from "./types";
import SortIcon from "./SortIcon";
import Notification from "../Notification";
const StyledTable = styled.table.attrs(() => ({
className: "table content is-hoverable"
}))``;
type Props = {
data: any[];
sortable?: boolean;
emptyMessage?: string;
children: Array<ReactElement>;
};
const Table: FC<Props> = ({ data, sortable, children, emptyMessage }) => {
const [tableData, setTableData] = useState(data);
useEffect(() => {
setTableData(data);
}, [data]);
const [ascending, setAscending] = useState(false);
const [lastSortBy, setlastSortBy] = useState<number | undefined>();
const [hoveredColumnIndex, setHoveredColumnIndex] = useState<number | undefined>();
const isSortable = (child: ReactElement) => {
return sortable && child.props.createComparator;
};
const sortFunctions: Comparator | undefined[] = [];
React.Children.forEach(children, (child, index) => {
if (child && isSortable(child)) {
sortFunctions.push(child.props.createComparator(child.props, index));
} else {
sortFunctions.push(undefined);
}
});
const mapDataToColumns = (row: any) => {
return (
<tr>
{React.Children.map(children, (child, columnIndex) => {
return <td>{React.cloneElement(child, { ...child.props, columnIndex, row })}</td>;
})}
</tr>
);
};
const sortDescending = (sortAscending: (a: any, b: any) => number) => {
return (a: any, b: any) => {
return sortAscending(a, b) * -1;
};
};
const tableSort = (index: number) => {
const sortFn = sortFunctions[index];
if (!sortFn) {
throw new Error(`column with index ${index} is not sortable`);
}
const sortableData = [...tableData];
let sortOrder = ascending;
if (lastSortBy !== index) {
setAscending(true);
sortOrder = true;
}
const sortFunction = sortOrder ? sortFn : sortDescending(sortFn);
sortableData.sort(sortFunction);
setTableData(sortableData);
setAscending(!sortOrder);
setlastSortBy(index);
};
const shouldShowIcon = (index: number) => {
return index === lastSortBy || index === hoveredColumnIndex;
};
if (!tableData || tableData.length <= 0) {
if (emptyMessage) {
return <Notification type="info">{emptyMessage}</Notification>;
} else {
return null;
}
}
return (
<StyledTable>
<thead>
<tr>
{React.Children.map(children, (child, index) => (
<th
className={isSortable(child) && "has-cursor-pointer"}
onClick={isSortable(child) ? () => tableSort(index) : undefined}
onMouseEnter={() => setHoveredColumnIndex(index)}
onMouseLeave={() => setHoveredColumnIndex(undefined)}
>
{child.props.header}
{isSortable(child) && renderSortIcon(child, ascending, shouldShowIcon(index))}
</th>
))}
</tr>
</thead>
<tbody>{tableData.map(mapDataToColumns)}</tbody>
</StyledTable>
);
};
Table.defaultProps = {
sortable: true
};
const renderSortIcon = (child: ReactElement, ascending: boolean, showIcon: boolean) => {
if (child.props.ascendingIcon && child.props.descendingIcon) {
return <SortIcon name={ascending ? child.props.ascendingIcon : child.props.descendingIcon} isVisible={showIcon} />;
} else {
return <SortIcon name={ascending ? "sort-amount-down-alt" : "sort-amount-down"} isVisible={showIcon} />;
}
};
export default Table;

View File

@@ -0,0 +1,21 @@
import React, { FC } from "react";
import { ColumnProps } from "./types";
import comparators from "../comparators";
type Props = ColumnProps & {
dataKey: string;
};
const TextColumn: FC<Props> = ({ row, dataKey }) => {
return row[dataKey];
};
TextColumn.defaultProps = {
createComparator: (props: Props) => {
return comparators.byKey(props.dataKey);
},
ascendingIcon: "sort-alpha-down-alt",
descendingIcon: "sort-alpha-down"
};
export default TextColumn;

View File

@@ -0,0 +1,4 @@
export { default as Table } from "./Table";
export { default as Column } from "./Column";
export { default as TextColumn } from "./TextColumn";
export { default as SortIcon } from "./SortIcon";

View File

@@ -0,0 +1,12 @@
import { ReactNode } from "react";
export type Comparator = (a: any, b: any) => number;
export type ColumnProps = {
header: ReactNode;
row?: any;
columnIndex?: number;
createComparator?: (props: any, columnIndex: number) => Comparator;
ascendingIcon?: string;
descendingIcon?: string;
};

View File

@@ -16,7 +16,7 @@
"@types/enzyme": "^3.10.3", "@types/enzyme": "^3.10.3",
"@types/jest": "^24.0.19", "@types/jest": "^24.0.19",
"@types/react": "^16.9.9", "@types/react": "^16.9.9",
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"babel": { "babel": {
"presets": [ "presets": [

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const { spawnSync } = require("child_process"); const { spawnSync } = require("child_process");
const commands = ["plugin", "plugin-watch", "publish"]; const commands = ["plugin", "plugin-watch", "publish", "version"];
const args = process.argv.slice(2); const args = process.argv.slice(2);

View File

@@ -14,8 +14,10 @@
"cache-loader": "^4.1.0", "cache-loader": "^4.1.0",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",
"file-loader": "^4.2.0", "file-loader": "^4.2.0",
"mini-css-extract-plugin": "^0.8.0",
"mustache": "^3.1.0", "mustache": "^3.1.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"script-loader": "^0.7.2", "script-loader": "^0.7.2",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",

View File

@@ -4,7 +4,7 @@ const versions = require("../versions");
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length < 1) { if (args.length < 1) {
console.log("usage ui-scripts publish version"); console.log("usage ui-scripts publish <version>");
process.exit(1); process.exit(1);
} }

View File

@@ -0,0 +1,12 @@
const lerna = require("../lerna");
const args = process.argv.slice(2);
if (args.length < 1) {
console.log("usage ui-scripts version <new-version>");
process.exit(1);
}
const version = args[0];
lerna.version(version);

View File

@@ -1,6 +1,8 @@
const path = require("path"); const path = require("path");
const createIndexMiddleware = require("./middleware/IndexMiddleware"); const createIndexMiddleware = require("./middleware/IndexMiddleware");
const createContextPathMiddleware = require("./middleware/ContextPathMiddleware"); const createContextPathMiddleware = require("./middleware/ContextPathMiddleware");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const root = path.resolve(process.cwd(), "scm-ui"); const root = path.resolve(process.cwd(), "scm-ui");
@@ -8,11 +10,7 @@ module.exports = [
{ {
context: root, context: root,
entry: { entry: {
webapp: [ webapp: [path.resolve(__dirname, "webpack-public-path.js"), "./ui-webapp/src/index.tsx"]
path.resolve(__dirname, "webpack-public-path.js"),
"./ui-styles/src/scm.scss",
"./ui-webapp/src/index.tsx"
]
}, },
devtool: "cheap-module-eval-source-map", devtool: "cheap-module-eval-source-map",
target: "web", target: "web",
@@ -109,6 +107,41 @@ module.exports = [
} }
} }
}, },
{
context: root,
entry: "./ui-styles/src/scm.scss",
module: {
rules: [
{
test: /\.(css|scss|sass)$/i,
use: [
{
loader: MiniCssExtractPlugin.loader
},
"css-loader",
"sass-loader"
]
},
{
test: /\.(png|svg|jpg|gif|woff2?|eot|ttf)$/,
use: ["file-loader"]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "ui-styles.css",
ignoreOrder: false
})
],
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
},
output: {
path: path.join(root, "target", "assets"),
filename: "ui-styles.bundle.js"
}
},
{ {
context: path.resolve(root), context: path.resolve(root),
entry: { entry: {

View File

@@ -622,6 +622,10 @@ form .field:not(.is-grouped) {
} }
} }
.help {
position: absolute;
}
// label with help-icon compensation // label with help-icon compensation
.label-icon-spacing { .label-icon-spacing {
margin-top: 30px; margin-top: 30px;
@@ -809,10 +813,6 @@ form .field:not(.is-grouped) {
} }
} }
.modal-card-body div div:last-child {
border-bottom: none;
}
// cursor // cursor
.has-cursor-pointer { .has-cursor-pointer {
cursor: pointer; cursor: pointer;

View File

@@ -26,6 +26,6 @@
"@types/enzyme": "^3.10.3", "@types/enzyme": "^3.10.3",
"@types/enzyme-adapter-react-16": "^1.0.5", "@types/enzyme-adapter-react-16": "^1.0.5",
"@types/jest": "^24.0.19", "@types/jest": "^24.0.19",
"typescript": "^3.6.4" "typescript": "^3.7.2"
} }
} }

View File

@@ -14,7 +14,7 @@
"typecheck": "tsc" "typecheck": "tsc"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^3.6.4" "typescript": "^3.7.2"
}, },
"babel": { "babel": {
"presets": [ "presets": [

View File

@@ -26,8 +26,7 @@
"systemjs": "0.21.6" "systemjs": "0.21.6"
}, },
"scripts": { "scripts": {
"test": "jest", "test": "jest"
"flow": "flow"
}, },
"devDependencies": { "devDependencies": {
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT", "@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",

View File

@@ -14,6 +14,7 @@
<base href="{{ contextPath }}"> <base href="{{ contextPath }}">
<title>SCM-Manager</title> <title>SCM-Manager</title>
<link rel="stylesheet" type="text/css" href="{{ contextPath }}/assets/ui-styles.css">
<script> <script>
var modernBrowser = ( var modernBrowser = (
'fetch' in window && 'fetch' in window &&

View File

@@ -91,8 +91,7 @@
"submit": "Speichern" "submit": "Speichern"
}, },
"delete": { "delete": {
"button": "Löschen", "button": "Berechtigungsrolle löschen",
"subtitle": "Berechtigungsrolle löschen",
"confirmAlert": { "confirmAlert": {
"title": "Berechtigungsrolle löschen?", "title": "Berechtigungsrolle löschen?",
"message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.", "message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.",

View File

@@ -60,8 +60,7 @@
} }
}, },
"deleteGroup": { "deleteGroup": {
"subtitle": "Gruppe löschen", "button": "Gruppe löschen",
"button": "Löschen",
"confirmAlert": { "confirmAlert": {
"title": "Gruppe löschen", "title": "Gruppe löschen",
"message": "Soll die Gruppe wirklich gelöscht werden?", "message": "Soll die Gruppe wirklich gelöscht werden?",

View File

@@ -166,8 +166,7 @@
} }
}, },
"deleteRepo": { "deleteRepo": {
"subtitle": "Repository löschen", "button": "Repository löschen",
"button": "Löschen",
"confirmAlert": { "confirmAlert": {
"title": "Repository löschen", "title": "Repository löschen",
"message": "Soll das Repository wirklich gelöscht werden?", "message": "Soll das Repository wirklich gelöscht werden?",
@@ -177,7 +176,8 @@
}, },
"diff": { "diff": {
"sideBySide": "Zweispaltig", "sideBySide": "Zweispaltig",
"combined": "Kombiniert" "combined": "Kombiniert",
"noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden."
}, },
"fileUpload": { "fileUpload": {
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.", "clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",

View File

@@ -45,8 +45,7 @@
"subtitle": "Erstellen eines neuen Benutzers" "subtitle": "Erstellen eines neuen Benutzers"
}, },
"deleteUser": { "deleteUser": {
"subtitle": "Benutzer löschen", "button": "Benutzer löschen",
"button": "Löschen",
"confirmAlert": { "confirmAlert": {
"title": "Benutzer löschen", "title": "Benutzer löschen",
"message": "Soll der Benutzer wirklich gelöscht werden?", "message": "Soll der Benutzer wirklich gelöscht werden?",

View File

@@ -11,7 +11,7 @@
"communityTitle": "Community Support", "communityTitle": "Community Support",
"communityIconAlt": "Community Support Icon", "communityIconAlt": "Community Support Icon",
"communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.", "communityInfo": "Contact the SCM-Manager support team for questions about SCM-Manager, to report bugs or to request features through the official channels.",
"communityButton": "Contact our team", "communityButton": "Contact our Team",
"enterpriseTitle": "Enterprise Support", "enterpriseTitle": "Enterprise Support",
"enterpriseIconAlt": "Enterprise Support Icon", "enterpriseIconAlt": "Enterprise Support Icon",
"enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?", "enterpriseInfo": "You require support with the integration of SCM-Manager into your processes, with the customization of the tool or simply a service level agreement (SLA)?",
@@ -76,8 +76,8 @@
"createSubtitle": "Create Permission Role", "createSubtitle": "Create Permission Role",
"editSubtitle": "Edit Permission Role", "editSubtitle": "Edit Permission Role",
"overview": { "overview": {
"title": "Overview of all permission roles", "title": "Overview of all Permission Roles",
"noPermissionRoles": "No permission roles found.", "noPermissionRoles": "No Permission Roles found.",
"createButton": "Create Permission Role" "createButton": "Create Permission Role"
}, },
"editButton": "Edit", "editButton": "Edit",
@@ -91,10 +91,9 @@
"submit": "Save" "submit": "Save"
}, },
"delete": { "delete": {
"button": "Delete", "button": "Delete Permission Role",
"subtitle": "Delete permission role",
"confirmAlert": { "confirmAlert": {
"title": "Delete permission role", "title": "Delete Permission Role",
"message": "Do you really want to delete this permission role? All users will lose their corresponding permissions.", "message": "Do you really want to delete this permission role? All users will lose their corresponding permissions.",
"submit": "Yes", "submit": "Yes",
"cancel": "No" "cancel": "No"

View File

@@ -60,8 +60,7 @@
} }
}, },
"deleteGroup": { "deleteGroup": {
"subtitle": "Delete Group", "button": "Delete Group",
"button": "Delete",
"confirmAlert": { "confirmAlert": {
"title": "Delete Group", "title": "Delete Group",
"message": "Do you really want to delete the group?", "message": "Do you really want to delete the group?",

View File

@@ -166,8 +166,7 @@
} }
}, },
"deleteRepo": { "deleteRepo": {
"subtitle": "Delete Repository", "button": "Delete Repository",
"button": "Delete",
"confirmAlert": { "confirmAlert": {
"title": "Delete repository", "title": "Delete repository",
"message": "Do you really want to delete the repository?", "message": "Do you really want to delete the repository?",
@@ -184,7 +183,8 @@
"copy": "copied" "copy": "copied"
}, },
"sideBySide": "side-by-side", "sideBySide": "side-by-side",
"combined": "combined" "combined": "combined",
"noDiffFound": "No Diff between the selected branches found."
}, },
"fileUpload": { "fileUpload": {
"clickHere": "Click here to select your file", "clickHere": "Click here to select your file",

View File

@@ -45,17 +45,16 @@
"subtitle": "Create a new user" "subtitle": "Create a new user"
}, },
"deleteUser": { "deleteUser": {
"subtitle": "Delete User", "button": "Delete User",
"button": "Delete",
"confirmAlert": { "confirmAlert": {
"title": "Delete user", "title": "Delete User",
"message": "Do you really want to delete the user?", "message": "Do you really want to delete the user?",
"submit": "Yes", "submit": "Yes",
"cancel": "No" "cancel": "No"
} }
}, },
"singleUserPassword": { "singleUserPassword": {
"button": "Set password", "button": "Set Password",
"setPasswordSuccessful": "Password successfully set" "setPasswordSuccessful": "Password successfully set"
}, },
"userForm": { "userForm": {

View File

@@ -86,8 +86,7 @@
"submit": "Guardar" "submit": "Guardar"
}, },
"delete": { "delete": {
"button": "Borrar", "button": "Eliminar el rol",
"subtitle": "Eliminar el rol",
"confirmAlert": { "confirmAlert": {
"title": "Eliminar el rol", "title": "Eliminar el rol",
"message": "¿Realmente desea borrar el rol? Todos los usuarios de este rol perderń sus permisos.", "message": "¿Realmente desea borrar el rol? Todos los usuarios de este rol perderń sus permisos.",

View File

@@ -60,8 +60,7 @@
} }
}, },
"deleteGroup": { "deleteGroup": {
"subtitle": "Borrar grupo", "button": "Borrar grupo",
"button": "Borrar",
"confirmAlert": { "confirmAlert": {
"title": "Borrar grupo", "title": "Borrar grupo",
"message": "¿Realmente desea borrar el grupo?", "message": "¿Realmente desea borrar el grupo?",

View File

@@ -166,8 +166,7 @@
} }
}, },
"deleteRepo": { "deleteRepo": {
"subtitle": "Borrar repositorio", "button": "Borrar repositorio",
"button": "Borrar",
"confirmAlert": { "confirmAlert": {
"title": "Borrar repositorio", "title": "Borrar repositorio",
"message": "¿Realmente desea borrar el repositorio?", "message": "¿Realmente desea borrar el repositorio?",
@@ -184,7 +183,8 @@
"copy": "copiado" "copy": "copiado"
}, },
"sideBySide": "dos columnas", "sideBySide": "dos columnas",
"combined": "combinado" "combined": "combinado",
"noDiffFound": "No se encontraron diferencias entre las ramas seleccionadas."
}, },
"fileUpload": { "fileUpload": {
"clickHere": "Haga click aquí para seleccionar su fichero", "clickHere": "Haga click aquí para seleccionar su fichero",

View File

@@ -45,8 +45,7 @@
"subtitle": "Crear un nuevo usuario" "subtitle": "Crear un nuevo usuario"
}, },
"deleteUser": { "deleteUser": {
"subtitle": "Borrar usuario", "button": "Borrar usuario",
"button": "Borrar",
"confirmAlert": { "confirmAlert": {
"title": "Borrar usuario", "title": "Borrar usuario",
"message": "¿Realmente desea borrar el usuario?", "message": "¿Realmente desea borrar el usuario?",

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { WithTranslation, withTranslation } from "react-i18next"; import { WithTranslation, withTranslation } from "react-i18next";
import { NamespaceStrategies, Config } from "@scm-manager/ui-types"; import { NamespaceStrategies, Config } from "@scm-manager/ui-types";
import { SubmitButton, Notification } from "@scm-manager/ui-components"; import { Level, SubmitButton, Notification } from "@scm-manager/ui-components";
import ProxySettings from "./ProxySettings"; import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings"; import GeneralSettings from "./GeneralSettings";
import BaseUrlSettings from "./BaseUrlSettings"; import BaseUrlSettings from "./BaseUrlSettings";
@@ -151,11 +151,15 @@ class ConfigForm extends React.Component<Props, State> {
hasUpdatePermission={configUpdatePermission} hasUpdatePermission={configUpdatePermission}
/> />
<hr /> <hr />
<Level
right={
<SubmitButton <SubmitButton
loading={loading} loading={loading}
label={t("config.form.submit")} label={t("config.form.submit")}
disabled={!configUpdatePermission || this.hasError() || !this.state.changed} disabled={!configUpdatePermission || this.hasError() || !this.state.changed}
/> />
}
/>
</form> </form>
); );
} }

View File

@@ -1,7 +1,7 @@
import { apiClient } from "@scm-manager/ui-components"; import { apiClient } from "@scm-manager/ui-components";
const waitForRestart = () => { const waitForRestart = () => {
const endTime = Number(new Date()) + 10000; const endTime = Number(new Date()) + 60000;
let started = false; let started = false;
const executor = (resolve, reject) => { const executor = (resolve, reject) => {

Some files were not shown because too many files have changed in this diff Show More