mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 09:46:16 +01:00
merge with default branch
This commit is contained in:
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@@ -7,7 +7,7 @@ import com.cloudogu.ces.cesbuildlib.*
|
||||
node('docker') {
|
||||
|
||||
// Change this as when we go back to default - necessary for proper SonarQube analysis
|
||||
mainBranch = '2.0.0-m3'
|
||||
mainBranch = 'default'
|
||||
|
||||
properties([
|
||||
// Keep only the last 10 build to preserve space
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"test": "lerna run --scope '@scm-manager/ui-*' test",
|
||||
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
|
||||
"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": {
|
||||
"babel-plugin-reflow": "^0.2.7",
|
||||
|
||||
@@ -99,8 +99,9 @@ import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
|
||||
import static javax.lang.model.util.ElementFilter.methodsIn;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@SupportedAnnotationTypes("*")
|
||||
@@ -372,6 +373,18 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
|
||||
attributes.put(entry.getKey().getSimpleName().toString(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<groupId>sonia.scm</groupId>
|
||||
<artifactId>scm-annotations</artifactId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
<name>scm-annotations</name>
|
||||
|
||||
@@ -17,7 +17,7 @@ public class ConcurrentModificationException extends ExceptionWithContext {
|
||||
this(Collections.singletonList(new ContextEntry(type, id)));
|
||||
}
|
||||
|
||||
private ConcurrentModificationException(List<ContextEntry> context) {
|
||||
public ConcurrentModificationException(List<ContextEntry> context) {
|
||||
super(context, createMessage(context));
|
||||
}
|
||||
|
||||
@@ -32,3 +32,4 @@ public class ConcurrentModificationException extends ExceptionWithContext {
|
||||
.collect(joining(" in ", "", " has been modified concurrently"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ public final class SCMContext
|
||||
*/
|
||||
public static final User ANONYMOUS = new User(USER_ANONYMOUS,
|
||||
"SCM Anonymous",
|
||||
"scm-anonymous@scm-manager.com");
|
||||
"scm-anonymous@scm-manager.org");
|
||||
|
||||
/** Singleton instance of {@link SCMContextProvider} */
|
||||
private static volatile SCMContextProvider provider;
|
||||
|
||||
@@ -134,7 +134,7 @@ public final class IterableQueue<T> implements Iterable<T>
|
||||
else
|
||||
{
|
||||
logger.trace("create queue iterator");
|
||||
iterator = new QueueIterator<T>(this);
|
||||
iterator = new QueueIterator<>(this);
|
||||
}
|
||||
|
||||
return iterator;
|
||||
|
||||
@@ -63,7 +63,7 @@ public class LimitedSortedSet<E> extends ForwardingSortedSet<E>
|
||||
*/
|
||||
public LimitedSortedSet(int maxSize)
|
||||
{
|
||||
this.sortedSet = new TreeSet<E>();
|
||||
this.sortedSet = new TreeSet<>();
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -183,5 +183,5 @@ public abstract class AbstractResourceProcessor implements ResourceProcessor
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private Map<String, String> variableMap = new HashMap<String, String>();
|
||||
private Map<String, String> variableMap = new HashMap<>();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class INIConfiguration
|
||||
*/
|
||||
public INIConfiguration()
|
||||
{
|
||||
this.sectionMap = new LinkedHashMap<String, INISection>();
|
||||
this.sectionMap = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@@ -55,7 +55,7 @@ public class INISection
|
||||
public INISection(String name)
|
||||
{
|
||||
this.name = name;
|
||||
this.parameters = new LinkedHashMap<String, String>();
|
||||
this.parameters = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,7 @@ public class AvailablePlugin implements Plugin {
|
||||
return pending;
|
||||
}
|
||||
|
||||
public AvailablePlugin install() {
|
||||
AvailablePlugin install() {
|
||||
Preconditions.checkState(!pending, "installation is already pending");
|
||||
return new AvailablePlugin(pluginDescriptor, true);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@ public abstract class AbstactImportHandler implements AdvancedImportHandler
|
||||
Repository repository = new Repository();
|
||||
|
||||
repository.setName(repositoryName);
|
||||
repository.setPublicReadable(false);
|
||||
repository.setType(getTypeName());
|
||||
|
||||
return repository;
|
||||
|
||||
@@ -83,8 +83,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
private String name;
|
||||
@XmlElement(name = "permission")
|
||||
private Set<RepositoryPermission> permissions = new HashSet<>();
|
||||
@XmlElement(name = "public")
|
||||
private boolean publicReadable = false;
|
||||
private String type;
|
||||
|
||||
|
||||
@@ -225,15 +223,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
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.
|
||||
* <ul>
|
||||
@@ -292,10 +281,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
return this.permissions.remove(permission);
|
||||
}
|
||||
|
||||
public void setPublicReadable(boolean publicReadable) {
|
||||
this.publicReadable = publicReadable;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
@@ -332,7 +317,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
repository.setLastModified(lastModified);
|
||||
repository.setDescription(description);
|
||||
repository.setPermissions(permissions);
|
||||
repository.setPublicReadable(publicReadable);
|
||||
|
||||
// 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(contact, other.contact)
|
||||
&& Objects.equal(description, other.description)
|
||||
&& Objects.equal(publicReadable, other.publicReadable)
|
||||
&& Objects.equal(permissions, other.permissions)
|
||||
&& Objects.equal(type, other.type)
|
||||
&& Objects.equal(creationDate, other.creationDate)
|
||||
@@ -371,7 +354,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
|
||||
@Override
|
||||
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,
|
||||
healthCheckFailures);
|
||||
}
|
||||
@@ -384,7 +367,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
.add("name", name)
|
||||
.add("contact", contact)
|
||||
.add("description", description)
|
||||
.add("publicReadable", publicReadable)
|
||||
.add("permissions", permissions)
|
||||
.add("type", type)
|
||||
.add("lastModified", lastModified)
|
||||
|
||||
@@ -46,7 +46,7 @@ public final class Base32 extends AbstractBase
|
||||
{
|
||||
|
||||
/** base value */
|
||||
private static final BigInteger BASE = BigInteger.valueOf(32l);
|
||||
private static final BigInteger BASE = BigInteger.valueOf(32L);
|
||||
|
||||
/** char table */
|
||||
private static final String CHARS = "0123456789bcdefghjkmnpqrstuvwxyz";
|
||||
|
||||
@@ -46,7 +46,7 @@ public final class Base62 extends AbstractBase
|
||||
{
|
||||
|
||||
/** base value */
|
||||
private static final BigInteger BASE = BigInteger.valueOf(62l);
|
||||
private static final BigInteger BASE = BigInteger.valueOf(62L);
|
||||
|
||||
/** char table */
|
||||
private static final String CHARS =
|
||||
|
||||
@@ -79,7 +79,7 @@ public final class LinkTextParser
|
||||
public static String parseText(String content)
|
||||
{
|
||||
Matcher m = REGEX_URL.matcher(content);
|
||||
List<Token> tokens = new ArrayList<Token>();
|
||||
List<Token> tokens = new ArrayList<>();
|
||||
int position = 0;
|
||||
String tokenContent = null;
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ public final class ServiceUtil
|
||||
*/
|
||||
public static <T> List<T> getServices(Class<T> type)
|
||||
{
|
||||
List<T> result = new ArrayList<T>();
|
||||
List<T> result = new ArrayList<>();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -38,23 +38,12 @@ package sonia.scm.util;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -273,12 +262,12 @@ public final class Util
|
||||
Comparator<T> comparator, CollectionAppender<T> appender, int start,
|
||||
int limit)
|
||||
{
|
||||
List<T> result = new ArrayList<T>();
|
||||
List<T> valueList = new ArrayList<T>(values);
|
||||
List<T> result = new ArrayList<>();
|
||||
List<T> valueList = new ArrayList<>(values);
|
||||
|
||||
if (comparator != null)
|
||||
{
|
||||
Collections.sort(valueList, comparator);
|
||||
valueList.sort(comparator);
|
||||
}
|
||||
|
||||
int length = valueList.size();
|
||||
@@ -506,12 +495,10 @@ public final class Util
|
||||
{
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < byteValue.length; i++)
|
||||
{
|
||||
int x = byteValue[i] & 0xff;
|
||||
for (final byte aByteValue : byteValue) {
|
||||
int x = aByteValue & 0xff;
|
||||
|
||||
if (x < 16)
|
||||
{
|
||||
if (x < 16) {
|
||||
buffer.append('0');
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public class EnvList
|
||||
*/
|
||||
public EnvList()
|
||||
{
|
||||
envMap = new HashMap<String, String>();
|
||||
envMap = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +77,7 @@ public class EnvList
|
||||
*/
|
||||
public EnvList(EnvList l)
|
||||
{
|
||||
envMap = new HashMap<String, String>(l.envMap);
|
||||
envMap = new HashMap<>(l.envMap);
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@@ -127,7 +127,7 @@ public class AuthenticationFilter extends HttpFilter
|
||||
logger.trace("user is already authenticated");
|
||||
processChain(request, response, chain, subject);
|
||||
}
|
||||
else if (isAnonymousAccessEnabled())
|
||||
else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request))
|
||||
{
|
||||
logger.trace("anonymous access granted");
|
||||
subject.login(new AnonymousToken());
|
||||
|
||||
@@ -470,13 +470,13 @@ public class BufferedHttpServletResponse extends HttpServletResponseWrapper
|
||||
private ByteArrayPrintWriter pw = null;
|
||||
|
||||
/** Field description */
|
||||
private Set<Cookie> cookies = new HashSet<Cookie>();
|
||||
private Set<Cookie> cookies = new HashSet<>();
|
||||
|
||||
/** Field description */
|
||||
private int statusCode = HttpServletResponse.SC_OK;
|
||||
|
||||
/** Field description */
|
||||
private Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
private Map<String, String> headers = new LinkedHashMap<>();
|
||||
|
||||
/** Field description */
|
||||
private String statusMessage;
|
||||
|
||||
@@ -40,28 +40,20 @@ import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Closer;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -175,10 +167,8 @@ public class ProxyServlet extends HttpServlet
|
||||
private void copyContent(HttpURLConnection con, HttpServletResponse response)
|
||||
throws IOException
|
||||
{
|
||||
Closer closer = Closer.create();
|
||||
|
||||
try
|
||||
{
|
||||
try (Closer closer = Closer.create()) {
|
||||
InputStream webToProxyBuf =
|
||||
closer.register(new BufferedInputStream(con.getInputStream()));
|
||||
OutputStream proxyToClientBuf =
|
||||
@@ -188,10 +178,6 @@ public class ProxyServlet extends HttpServlet
|
||||
|
||||
logger.trace("copied {} bytes for proxy", bytes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
closer.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -102,7 +102,7 @@ public class XmlMapStringAdapter
|
||||
public Map<String, String> unmarshal(XmlMapStringElement[] elements)
|
||||
throws Exception
|
||||
{
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
Map<String, String> map = new HashMap<>();
|
||||
|
||||
if (elements != null)
|
||||
{
|
||||
|
||||
@@ -90,7 +90,7 @@ public class XmlSetStringAdapter extends XmlAdapter<String, Set<String>>
|
||||
@Override
|
||||
public Set<String> unmarshal(String rawString) throws Exception
|
||||
{
|
||||
Set<String> tokens = new HashSet<String>();
|
||||
Set<String> tokens = new HashSet<>();
|
||||
|
||||
for (String token : rawString.split(","))
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ public class IterableQueueTest
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testDuplicatedEndReached()
|
||||
{
|
||||
IterableQueue<String> queue = new IterableQueue<String>();
|
||||
IterableQueue<String> queue = new IterableQueue<>();
|
||||
|
||||
queue.endReached();
|
||||
queue.endReached();
|
||||
@@ -76,7 +76,7 @@ public class IterableQueueTest
|
||||
@Test
|
||||
public void testIterator()
|
||||
{
|
||||
IterableQueue<String> queue = new IterableQueue<String>();
|
||||
IterableQueue<String> queue = new IterableQueue<>();
|
||||
|
||||
assertEquals(QueueIterator.class, queue.iterator().getClass());
|
||||
queue.endReached();
|
||||
@@ -120,7 +120,7 @@ public class IterableQueueTest
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testPushEndReached()
|
||||
{
|
||||
IterableQueue<String> queue = new IterableQueue<String>();
|
||||
IterableQueue<String> queue = new IterableQueue<>();
|
||||
|
||||
queue.push("a");
|
||||
queue.endReached();
|
||||
@@ -134,7 +134,7 @@ public class IterableQueueTest
|
||||
@Test
|
||||
public void testSingleConsumer()
|
||||
{
|
||||
final IterableQueue<Integer> queue = new IterableQueue<Integer>();
|
||||
final IterableQueue<Integer> queue = new IterableQueue<>();
|
||||
|
||||
new Thread(new IntegerProducer(queue, false, 100)).start();
|
||||
assertResult(Lists.newArrayList(queue), 100);
|
||||
@@ -176,12 +176,12 @@ public class IterableQueueTest
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threads);
|
||||
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++)
|
||||
{
|
||||
Future<List<Integer>> future =
|
||||
executor.submit(new CallableQueueCollector<Integer>(queue));
|
||||
executor.submit(new CallableQueueCollector<>(queue));
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ public class ScmModuleTest
|
||||
assertThat(
|
||||
module.getExtensions(),
|
||||
containsInAnyOrder(
|
||||
(Class<?>) String.class,
|
||||
(Class<?>) Integer.class
|
||||
String.class,
|
||||
Integer.class
|
||||
)
|
||||
);
|
||||
assertThat(
|
||||
@@ -86,8 +86,8 @@ public class ScmModuleTest
|
||||
assertThat(
|
||||
module.getEvents(),
|
||||
containsInAnyOrder(
|
||||
(Class<?>) String.class,
|
||||
(Class<?>) Boolean.class
|
||||
String.class,
|
||||
Boolean.class
|
||||
)
|
||||
);
|
||||
assertThat(
|
||||
@@ -100,15 +100,15 @@ public class ScmModuleTest
|
||||
assertThat(
|
||||
module.getRestProviders(),
|
||||
containsInAnyOrder(
|
||||
(Class<?>) Integer.class,
|
||||
(Class<?>) Long.class
|
||||
Integer.class,
|
||||
Long.class
|
||||
)
|
||||
);
|
||||
assertThat(
|
||||
module.getRestResources(),
|
||||
containsInAnyOrder(
|
||||
(Class<?>) Float.class,
|
||||
(Class<?>) Double.class
|
||||
Float.class,
|
||||
Double.class
|
||||
)
|
||||
);
|
||||
//J+
|
||||
|
||||
@@ -128,7 +128,7 @@ public class TemplateEngineFactoryTest
|
||||
assertTrue(engines.contains(engine1));
|
||||
assertTrue(engines.contains(engine2));
|
||||
|
||||
Set<TemplateEngine> ce = new HashSet<TemplateEngine>();
|
||||
Set<TemplateEngine> ce = new HashSet<>();
|
||||
|
||||
ce.add(engine1);
|
||||
factory = new TemplateEngineFactory(ce, engine2);
|
||||
|
||||
@@ -56,7 +56,7 @@ public class UrlBuilderTest
|
||||
UrlBuilder builder = new UrlBuilder("http://www.short.de");
|
||||
|
||||
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());
|
||||
builder.appendParameter("c", "a b");
|
||||
assertEquals("http://www.short.de?i=123&s=abc&b=true&l=321&c=a%20b", builder.toString());
|
||||
|
||||
@@ -199,7 +199,7 @@ public class XmlGroupDatabase implements XmlDatabase<Group>
|
||||
/** Field description */
|
||||
@XmlJavaTypeAdapter(XmlGroupMapAdapter.class)
|
||||
@XmlElement(name = "groups")
|
||||
private Map<String, Group> groupMap = new LinkedHashMap<String, Group>();
|
||||
private Map<String, Group> groupMap = new LinkedHashMap<>();
|
||||
|
||||
/** Field description */
|
||||
private Long lastModified;
|
||||
|
||||
@@ -72,7 +72,7 @@ public class XmlGroupList implements Iterable<Group>
|
||||
*/
|
||||
public XmlGroupList(Map<String, Group> groupMap)
|
||||
{
|
||||
this.groups = new LinkedList<Group>(groupMap.values());
|
||||
this.groups = new LinkedList<>(groupMap.values());
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@@ -81,7 +81,7 @@ public class XmlGroupMapAdapter
|
||||
@Override
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -202,5 +202,5 @@ public class XmlUserDatabase implements XmlDatabase<User>
|
||||
/** Field description */
|
||||
@XmlJavaTypeAdapter(XmlUserMapAdapter.class)
|
||||
@XmlElement(name = "users")
|
||||
private Map<String, User> userMap = new LinkedHashMap<String, User>();
|
||||
private Map<String, User> userMap = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class XmlUserList implements Iterable<User>
|
||||
*/
|
||||
public XmlUserList(Map<String, User> userMap)
|
||||
{
|
||||
this.users = new LinkedList<User>(userMap.values());
|
||||
this.users = new LinkedList<>(userMap.values());
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@@ -81,7 +81,7 @@ public class XmlUserMapAdapter
|
||||
@Override
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -52,7 +52,9 @@ public final class XmlStreams {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.assertj.core.api.AbstractCharSequenceAssert;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
@@ -28,6 +29,7 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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_USERNAME;
|
||||
|
||||
@@ -94,8 +96,7 @@ public class DiffITCase {
|
||||
String gitDiff = getDiff(RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a"), gitRepositoryResponse);
|
||||
|
||||
String expected = getGitDiffWithoutIndexLine(gitDiff);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(expected);
|
||||
assertDiffsAreEqual(svnDiff, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,8 +108,7 @@ public class DiffITCase {
|
||||
String gitDiff = getDiff(RepositoryUtil.removeAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt"), gitRepositoryResponse);
|
||||
|
||||
String expected = getGitDiffWithoutIndexLine(gitDiff);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(expected);
|
||||
assertDiffsAreEqual(svnDiff, expected);
|
||||
}
|
||||
|
||||
@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 expected = getGitDiffWithoutIndexLine(gitDiff);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(expected);
|
||||
assertDiffsAreEqual(svnDiff, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -161,21 +160,17 @@ public class DiffITCase {
|
||||
String fileContent = getFileContent("/diff/largefile/original/SvnDiffGenerator_forTest");
|
||||
String svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
|
||||
String gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
|
||||
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
|
||||
|
||||
fileContent = getFileContent("/diff/largefile/modified/v1/SvnDiffGenerator_forTest");
|
||||
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
|
||||
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
|
||||
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
|
||||
|
||||
fileContent = getFileContent("/diff/largefile/modified/v2/SvnDiffGenerator_forTest");
|
||||
svnDiff = getDiff(RepositoryUtil.updateAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, fileContent), svnRepositoryResponse);
|
||||
gitDiff = getDiff(RepositoryUtil.updateAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, fileName, fileContent), gitRepositoryResponse);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
|
||||
|
||||
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,8 +191,7 @@ public class DiffITCase {
|
||||
Changeset commit1 = RepositoryUtil.addFileAndCommit(gitRepositoryClient, fileName, ADMIN_USERNAME, "");
|
||||
String svnDiff = getDiff(commit, svnRepositoryResponse);
|
||||
String gitDiff = getDiff(commit1, gitRepositoryResponse);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(getGitDiffWithoutIndexLine(gitDiff));
|
||||
assertDiffsAreEqual(svnDiff, getGitDiffWithoutIndexLine(gitDiff));
|
||||
|
||||
}
|
||||
|
||||
@@ -218,8 +212,7 @@ public class DiffITCase {
|
||||
String gitDiff = getDiff(RepositoryUtil.addFileAndCommit(gitRepositoryClient, newFileName, ADMIN_USERNAME, "renamed file"), gitRepositoryResponse);
|
||||
|
||||
String expected = getGitDiffWithoutIndexLine(gitDiff);
|
||||
assertThat(svnDiff)
|
||||
.isEqualTo(expected);
|
||||
assertDiffsAreEqual(svnDiff, expected);
|
||||
}
|
||||
|
||||
public String getFileContent(String name) throws URISyntaxException, IOException {
|
||||
@@ -242,6 +235,12 @@ public class DiffITCase {
|
||||
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) {
|
||||
return svnRepositoryResponse.requestChangesets()
|
||||
.requestDiffInGitFormat(svnChangeset.getId())
|
||||
|
||||
@@ -213,8 +213,14 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
|
||||
if (lfsPointer.isPresent()) {
|
||||
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());
|
||||
}
|
||||
} else {
|
||||
file.setLength(loader.getSize());
|
||||
}
|
||||
|
||||
@@ -145,7 +145,12 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
|
||||
|
||||
private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException {
|
||||
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(treeWalk);
|
||||
return new BlobLoader(blob);
|
||||
|
||||
@@ -33,13 +33,18 @@ package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.diff.DiffEntry;
|
||||
import org.eclipse.jgit.diff.DiffFormatter;
|
||||
import org.eclipse.jgit.util.QuotedString;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
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);
|
||||
|
||||
return output -> {
|
||||
try (DiffFormatter formatter = new DiffFormatter(output)) {
|
||||
try (DiffFormatter formatter = new DiffFormatter(new DequoteOutputStream(output))) {
|
||||
formatter.setRepository(repository);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ const Switcher = styled(ButtonAddons)`
|
||||
right: 0;
|
||||
`;
|
||||
|
||||
const SmallButton = styled(Button).attrs(props => ({
|
||||
className: "is-small"
|
||||
}))`
|
||||
height: inherit;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
};
|
||||
@@ -62,9 +68,9 @@ export default class ProtocolInformation extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button color={color} action={() => this.selectProtocol(protocol)}>
|
||||
<SmallButton color={color} action={() => this.selectProtocol(protocol)}>
|
||||
{name.toUpperCase()}
|
||||
</Button>
|
||||
</SmallButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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\";");
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ public class HgPackageReader
|
||||
*/
|
||||
private void filterPackage(HgPackages packages)
|
||||
{
|
||||
List<HgPackage> pkgList = new ArrayList<HgPackage>();
|
||||
List<HgPackage> pkgList = new ArrayList<>();
|
||||
|
||||
for (HgPackage pkg : packages)
|
||||
{
|
||||
@@ -228,7 +228,7 @@ public class HgPackageReader
|
||||
if (packages == null)
|
||||
{
|
||||
packages = new HgPackages();
|
||||
packages.setPackages(new ArrayList<HgPackage>());
|
||||
packages.setPackages(new ArrayList<>());
|
||||
}
|
||||
|
||||
return packages;
|
||||
|
||||
@@ -77,7 +77,9 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
|
||||
String revision = MoreObjects.firstNonNull(request.getRevision(), "tip");
|
||||
Changeset c = LogCommand.on(getContext().open()).rev(revision).limit(1).single();
|
||||
|
||||
if (c != null) {
|
||||
cmd.rev(c.getNode());
|
||||
}
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getPath()))
|
||||
{
|
||||
@@ -100,6 +102,6 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
|
||||
}
|
||||
|
||||
FileObject file = cmd.execute();
|
||||
return new BrowserResult(c.getNode(), revision, file);
|
||||
return new BrowserResult(c == null? "tip": c.getNode(), revision, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,15 +211,7 @@ public class SVNKitLogger extends SVNDebugLogAdapter
|
||||
*/
|
||||
private Logger getLogger(SVNLogType type)
|
||||
{
|
||||
Logger logger = loggerMap.get(type);
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
logger = LoggerFactory.getLogger(parseName(type.getName()));
|
||||
loggerMap.put(type, logger);
|
||||
}
|
||||
|
||||
return logger;
|
||||
return loggerMap.computeIfAbsent(type, t -> LoggerFactory.getLogger(parseName(t.getName())));
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@@ -81,15 +81,7 @@ public class MapCacheManager
|
||||
@Override
|
||||
public synchronized <K, V> MapCache<K, V> getCache(String name)
|
||||
{
|
||||
MapCache<K, V> cache = cacheMap.get(name);
|
||||
|
||||
if (cache == null)
|
||||
{
|
||||
cache = new MapCache<K, V>();
|
||||
cacheMap.put(name, cache);
|
||||
}
|
||||
|
||||
return cache;
|
||||
return (MapCache<K, V>) cacheMap.computeIfAbsent(name, k -> new MapCache<K, V>());
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<name>scm-ui</name>
|
||||
|
||||
<properties>
|
||||
<build.script>build</build.script>
|
||||
<sonar.language>typescript</sonar.language>
|
||||
<sonar.sources>ui-extensions/src,ui-components/src,ui-webapp/src</sonar.sources>
|
||||
<sonar.test.exclusions>**/*.test.js,src/tests/**</sonar.test.exclusions>
|
||||
@@ -68,7 +69,7 @@
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<script>build</script>
|
||||
<script>${build.script}</script>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
@@ -118,4 +119,21 @@
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>dev</id>
|
||||
|
||||
<activation>
|
||||
<property>
|
||||
<name>development</name>
|
||||
</property>
|
||||
</activation>
|
||||
|
||||
<properties>
|
||||
<build.script>build:dev</build.script>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"private": false,
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"prettier": "^1.18.2"
|
||||
"prettier": "^1.19.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"private": false,
|
||||
"main": "tsconfig.json",
|
||||
"dependencies": {
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"raf": "^3.4.0",
|
||||
"react-test-renderer": "^16.10.2",
|
||||
"storybook-addon-i18next": "^1.2.1",
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-extensions": "^2.0.0-SNAPSHOT",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Async, AsyncCreatable } from "react-select";
|
||||
import { SelectValue } from "@scm-manager/ui-types";
|
||||
import LabelWithHelpIcon from "./forms/LabelWithHelpIcon";
|
||||
@@ -7,13 +8,14 @@ import { ActionMeta, ValueType } from "react-select/lib/types";
|
||||
type Props = {
|
||||
loadSuggestions: (p: string) => Promise<SelectValue[]>;
|
||||
valueSelected: (p: SelectValue) => void;
|
||||
label: string;
|
||||
label?: string;
|
||||
helpText?: string;
|
||||
value?: SelectValue;
|
||||
placeholder: string;
|
||||
loadingMessage: string;
|
||||
noOptionsMessage: string;
|
||||
creatable?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type State = {};
|
||||
@@ -53,10 +55,11 @@ class Autocomplete extends React.Component<Props, State> {
|
||||
loadingMessage,
|
||||
noOptionsMessage,
|
||||
loadSuggestions,
|
||||
creatable
|
||||
creatable,
|
||||
className
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<div className={classNames("field", className)}>
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
{creatable ? (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { SelectValue, AutocompleteObject } from "@scm-manager/ui-types";
|
||||
import Autocomplete from "./Autocomplete";
|
||||
import { apiClient } from "./apiclient";
|
||||
|
||||
export type AutocompleteProps = {
|
||||
autocompleteLink?: string;
|
||||
@@ -19,7 +20,8 @@ export default class UserGroupAutocomplete extends React.Component<Props> {
|
||||
loadSuggestions = (inputValue: string): Promise<SelectValue[]> => {
|
||||
const url = this.props.autocompleteLink;
|
||||
const link = url + "?q=";
|
||||
return fetch(link + inputValue)
|
||||
return apiClient
|
||||
.get(link + inputValue)
|
||||
.then(response => response.json())
|
||||
.then((json: AutocompleteObject[]) => {
|
||||
return json.map(element => {
|
||||
|
||||
@@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||
<div
|
||||
className="sc-gipzik xsalO"
|
||||
className="sc-fBuWsC ldmpJA"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -381,7 +381,7 @@ exports[`Storyshots Forms|Checkbox Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||
<div
|
||||
className="sc-gipzik xsalO"
|
||||
className="sc-fBuWsC ldmpJA"
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
@@ -409,7 +409,7 @@ exports[`Storyshots Forms|Checkbox Disabled 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Radio Default 1`] = `
|
||||
<div
|
||||
className="sc-csuQGl fFFkRK"
|
||||
className="sc-fMiknA keSQNk"
|
||||
>
|
||||
<label
|
||||
className="radio"
|
||||
@@ -438,7 +438,7 @@ exports[`Storyshots Forms|Radio Default 1`] = `
|
||||
|
||||
exports[`Storyshots Forms|Radio Disabled 1`] = `
|
||||
<div
|
||||
className="sc-csuQGl fFFkRK"
|
||||
className="sc-fMiknA keSQNk"
|
||||
>
|
||||
<label
|
||||
className="radio"
|
||||
@@ -2311,3 +2311,173 @@ PORT_NUMBER =
|
||||
</pre>
|
||||
</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>
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { apiClient, createUrl } from "./apiclient";
|
||||
import { apiClient, createUrl, extractXsrfTokenFromCookie } from "./apiclient";
|
||||
import fetchMock from "fetch-mock";
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,14 +9,52 @@ const sessionId = (
|
||||
.substr(2, 5)
|
||||
).toUpperCase();
|
||||
|
||||
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
|
||||
o.credentials = "same-origin";
|
||||
o.headers = {
|
||||
Cache: "no-cache",
|
||||
// identify the request as ajax request
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-SCM-Session-ID": sessionId
|
||||
const extractXsrfTokenFromJwt = (jwt: string) => {
|
||||
const parts = jwt.split(".");
|
||||
if (parts.length === 3) {
|
||||
return JSON.parse(atob(parts[1])).xsrf;
|
||||
}
|
||||
};
|
||||
|
||||
// @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;
|
||||
};
|
||||
|
||||
@@ -55,23 +93,32 @@ class ApiClient {
|
||||
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
|
||||
}
|
||||
|
||||
post(url: string, payload: any, contentType = "application/json") {
|
||||
return this.httpRequestWithJSONBody("POST", url, contentType, payload);
|
||||
post(url: string, payload?: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
|
||||
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();
|
||||
fileAppender(formData);
|
||||
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
body: formData
|
||||
body: formData,
|
||||
headers: additionalHeaders
|
||||
};
|
||||
return this.httpRequestWithBinaryBody(options, url);
|
||||
}
|
||||
|
||||
put(url: string, payload: any, contentType = "application/json") {
|
||||
return this.httpRequestWithJSONBody("PUT", url, contentType, payload);
|
||||
put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
|
||||
return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
|
||||
}
|
||||
|
||||
head(url: string) {
|
||||
@@ -90,21 +137,44 @@ class ApiClient {
|
||||
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 = {
|
||||
method: method,
|
||||
body: JSON.stringify(payload)
|
||||
headers: additionalHeaders
|
||||
};
|
||||
if (payload) {
|
||||
options.body = JSON.stringify(payload);
|
||||
}
|
||||
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) {
|
||||
options = applyFetchOptions(options);
|
||||
if (contentType) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export type ButtonProps = {
|
||||
|
||||
type Props = ButtonProps &
|
||||
RouteComponentProps & {
|
||||
title?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
color?: string;
|
||||
};
|
||||
@@ -38,7 +39,19 @@ class Button extends React.Component<Props> {
|
||||
};
|
||||
|
||||
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 fullWidthClass = fullWidth ? "is-fullwidth" : "";
|
||||
const reducedMobileClass = reducedMobile ? "is-reduced-mobile" : "";
|
||||
@@ -46,6 +59,7 @@ class Button extends React.Component<Props> {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={this.onClick}
|
||||
className={classNames("button", "is-" + color, loadingClass, fullWidthClass, reducedMobileClass, className)}
|
||||
@@ -63,6 +77,7 @@ class Button extends React.Component<Props> {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={this.onClick}
|
||||
className={classNames("button", "is-" + color, loadingClass, fullWidthClass, className)}
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from "react";
|
||||
|
||||
type Props = {
|
||||
displayName: string;
|
||||
url: string;
|
||||
disabled: boolean;
|
||||
url?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ const buttonStory = (name: string, storyFn: () => ReactElement) => {
|
||||
.addDecorator(SpacingDecorator)
|
||||
.add("Default", storyFn);
|
||||
};
|
||||
|
||||
buttonStory("AddButton", () => <AddButton>Add</AddButton>);
|
||||
buttonStory("CreateButton", () => <CreateButton>Create</CreateButton>);
|
||||
buttonStory("DeleteButton", () => <DeleteButton>Delete</DeleteButton>);
|
||||
|
||||
126
scm-ui/ui-components/src/comparators.test.ts
Normal file
126
scm-ui/ui-components/src/comparators.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
75
scm-ui/ui-components/src/comparators.ts
Normal file
75
scm-ui/ui-components/src/comparators.ts
Normal 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
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from "react";
|
||||
import React, { FormEvent } from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Links, Link } from "@scm-manager/ui-types";
|
||||
import { apiClient, SubmitButton, Loading, ErrorNotification } from "../";
|
||||
import { FormEvent } from "react";
|
||||
import { apiClient, Level, SubmitButton, Loading, ErrorNotification } from "../";
|
||||
|
||||
type RenderProps = {
|
||||
readOnly: boolean;
|
||||
@@ -179,7 +178,9 @@ class Configuration extends React.Component<Props, State> {
|
||||
<form onSubmit={this.modifyConfiguration}>
|
||||
{this.props.render(renderProps)}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { MouseEvent } from "react";
|
||||
|
||||
import { AddButton } from "../buttons";
|
||||
import styled from "styled-components";
|
||||
import Level from "../layout/Level";
|
||||
import InputField from "./InputField";
|
||||
import AddButton from "../buttons/AddButton";
|
||||
|
||||
type Props = {
|
||||
addEntry: (p: string) => void;
|
||||
@@ -17,6 +18,22 @@ type State = {
|
||||
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> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -37,8 +54,9 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { disabled, buttonLabel, fieldLabel, errorMessage, helpText } = this.props;
|
||||
return (
|
||||
<>
|
||||
<InputField
|
||||
<StyledLevel
|
||||
children={
|
||||
<StyledInputField
|
||||
label={fieldLabel}
|
||||
errorMessage={errorMessage}
|
||||
onChange={this.handleAddEntryChange}
|
||||
@@ -48,12 +66,17 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
disabled={disabled}
|
||||
helpText={helpText}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<StyledField>
|
||||
<AddButton
|
||||
label={buttonLabel}
|
||||
action={this.addButtonClicked}
|
||||
disabled={disabled || this.state.entryToAdd === "" || !this.isValid()}
|
||||
/>
|
||||
</>
|
||||
</StyledField>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { MouseEvent } from "react";
|
||||
|
||||
import { AutocompleteObject, SelectValue } from "@scm-manager/ui-types";
|
||||
import styled from "styled-components";
|
||||
import { SelectValue } from "@scm-manager/ui-types";
|
||||
import Level from "../layout/Level";
|
||||
import Autocomplete from "../Autocomplete";
|
||||
import AddButton from "../buttons/AddButton";
|
||||
|
||||
@@ -20,6 +21,11 @@ type State = {
|
||||
selectedValue?: SelectValue;
|
||||
};
|
||||
|
||||
const StyledAutocomplete = styled(Autocomplete)`
|
||||
width: 100%;
|
||||
margin-right: 1.5rem;
|
||||
`;
|
||||
|
||||
class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -41,8 +47,9 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
||||
|
||||
const { selectedValue } = this.state;
|
||||
return (
|
||||
<div className="field">
|
||||
<Autocomplete
|
||||
<Level
|
||||
children={
|
||||
<StyledAutocomplete
|
||||
label={fieldLabel}
|
||||
loadSuggestions={loadSuggestions}
|
||||
valueSelected={this.handleAddEntryChange}
|
||||
@@ -53,9 +60,13 @@ class AutocompleteAddEntryToTableField extends React.Component<Props, State> {
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
creatable={true}
|
||||
/>
|
||||
|
||||
}
|
||||
right={
|
||||
<div className="field">
|
||||
<AddButton label={buttonLabel} action={this.addButtonClicked} disabled={disabled} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ type Props = {
|
||||
errorMessage?: string;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
class InputField extends React.Component<Props> {
|
||||
@@ -47,11 +48,21 @@ class InputField extends React.Component<Props> {
|
||||
};
|
||||
|
||||
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 helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
|
||||
return (
|
||||
<div className="field">
|
||||
<div className={classNames("field", className)}>
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<input
|
||||
|
||||
@@ -52,6 +52,8 @@ export { default as OverviewPageActions } from "./OverviewPageActions";
|
||||
export { default as CardColumnGroup } from "./CardColumnGroup";
|
||||
export { default as CardColumn } from "./CardColumn";
|
||||
|
||||
export { default as comparators } from "./comparators";
|
||||
|
||||
export { apiClient } from "./apiclient";
|
||||
export * from "./errors";
|
||||
|
||||
@@ -63,6 +65,7 @@ export * from "./layout";
|
||||
export * from "./modals";
|
||||
export * from "./navigation";
|
||||
export * from "./repos";
|
||||
export * from "./table";
|
||||
|
||||
export {
|
||||
File,
|
||||
|
||||
@@ -4,15 +4,22 @@ import classNames from "classnames";
|
||||
type Props = {
|
||||
className?: string;
|
||||
left?: ReactNode;
|
||||
children?: ReactNode;
|
||||
right?: ReactNode;
|
||||
};
|
||||
|
||||
export default class Level extends React.Component<Props> {
|
||||
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 (
|
||||
<div className={classNames("level", className)}>
|
||||
<div className="level-left">{left}</div>
|
||||
{child}
|
||||
<div className="level-right">{right}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from "react";
|
||||
import DiffFile from "./DiffFile";
|
||||
import { DiffObjectProps, File } from "./DiffTypes";
|
||||
import Notification from "../Notification";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type Props = DiffObjectProps & {
|
||||
type Props = WithTranslation &
|
||||
DiffObjectProps & {
|
||||
diff: File[];
|
||||
defaultCollapse?: boolean;
|
||||
};
|
||||
@@ -13,15 +16,17 @@ class Diff extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { diff, ...fileProps } = this.props;
|
||||
const { diff, t, ...fileProps } = this.props;
|
||||
return (
|
||||
<>
|
||||
{diff.map((file, index) => (
|
||||
<DiffFile key={index} file={file} {...fileProps} {...this.props} />
|
||||
))}
|
||||
{diff.length === 0 ? (
|
||||
<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);
|
||||
|
||||
@@ -43,6 +43,7 @@ class LoadingDiff extends React.Component<Props, State> {
|
||||
|
||||
fetchDiff = () => {
|
||||
const { url } = this.props;
|
||||
this.setState({loading: true});
|
||||
apiClient
|
||||
.get(url)
|
||||
.then(response => response.text())
|
||||
|
||||
18
scm-ui/ui-components/src/table/Column.tsx
Normal file
18
scm-ui/ui-components/src/table/Column.tsx
Normal 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;
|
||||
19
scm-ui/ui-components/src/table/SortIcon.tsx
Normal file
19
scm-ui/ui-components/src/table/SortIcon.tsx
Normal 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;
|
||||
53
scm-ui/ui-components/src/table/Table.stories.tsx
Normal file
53
scm-ui/ui-components/src/table/Table.stories.tsx
Normal 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>
|
||||
));
|
||||
120
scm-ui/ui-components/src/table/Table.tsx
Normal file
120
scm-ui/ui-components/src/table/Table.tsx
Normal 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;
|
||||
21
scm-ui/ui-components/src/table/TextColumn.tsx
Normal file
21
scm-ui/ui-components/src/table/TextColumn.tsx
Normal 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;
|
||||
4
scm-ui/ui-components/src/table/index.ts
Normal file
4
scm-ui/ui-components/src/table/index.ts
Normal 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";
|
||||
12
scm-ui/ui-components/src/table/types.ts
Normal file
12
scm-ui/ui-components/src/table/types.ts
Normal 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;
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@types/react": "^16.9.9",
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
const commands = ["plugin", "plugin-watch", "publish"];
|
||||
const commands = ["plugin", "plugin-watch", "publish", "version"];
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
"cache-loader": "^4.1.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"file-loader": "^4.2.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"mustache": "^3.1.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"sass-loader": "^8.0.0",
|
||||
"script-loader": "^0.7.2",
|
||||
"style-loader": "^1.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ const versions = require("../versions");
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1) {
|
||||
console.log("usage ui-scripts publish version");
|
||||
console.log("usage ui-scripts publish <version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
12
scm-ui/ui-scripts/src/commands/version.js
Normal file
12
scm-ui/ui-scripts/src/commands/version.js
Normal 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);
|
||||
@@ -1,6 +1,8 @@
|
||||
const path = require("path");
|
||||
const createIndexMiddleware = require("./middleware/IndexMiddleware");
|
||||
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");
|
||||
|
||||
@@ -8,11 +10,7 @@ module.exports = [
|
||||
{
|
||||
context: root,
|
||||
entry: {
|
||||
webapp: [
|
||||
path.resolve(__dirname, "webpack-public-path.js"),
|
||||
"./ui-styles/src/scm.scss",
|
||||
"./ui-webapp/src/index.tsx"
|
||||
]
|
||||
webapp: [path.resolve(__dirname, "webpack-public-path.js"), "./ui-webapp/src/index.tsx"]
|
||||
},
|
||||
devtool: "cheap-module-eval-source-map",
|
||||
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),
|
||||
entry: {
|
||||
|
||||
@@ -622,6 +622,10 @@ form .field:not(.is-grouped) {
|
||||
}
|
||||
}
|
||||
|
||||
.help {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
// label with help-icon compensation
|
||||
.label-icon-spacing {
|
||||
margin-top: 30px;
|
||||
@@ -809,10 +813,6 @@ form .field:not(.is-grouped) {
|
||||
}
|
||||
}
|
||||
|
||||
.modal-card-body div div:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
// cursor
|
||||
.has-cursor-pointer {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
"@types/jest": "^24.0.19",
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.2"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
"systemjs": "0.21.6"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"flow": "flow"
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-tests": "^2.0.0-SNAPSHOT",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<base href="{{ contextPath }}">
|
||||
<title>SCM-Manager</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ contextPath }}/assets/ui-styles.css">
|
||||
<script>
|
||||
var modernBrowser = (
|
||||
'fetch' in window &&
|
||||
|
||||
@@ -91,8 +91,7 @@
|
||||
"submit": "Speichern"
|
||||
},
|
||||
"delete": {
|
||||
"button": "Löschen",
|
||||
"subtitle": "Berechtigungsrolle löschen",
|
||||
"button": "Berechtigungsrolle löschen",
|
||||
"confirmAlert": {
|
||||
"title": "Berechtigungsrolle löschen?",
|
||||
"message": "Wollen Sie diese Rolle wirklich löschen? Alle Benutzer mit dieser Rolle verlieren die entsprechenden Berechtigungen.",
|
||||
|
||||
@@ -60,8 +60,7 @@
|
||||
}
|
||||
},
|
||||
"deleteGroup": {
|
||||
"subtitle": "Gruppe löschen",
|
||||
"button": "Löschen",
|
||||
"button": "Gruppe löschen",
|
||||
"confirmAlert": {
|
||||
"title": "Gruppe löschen",
|
||||
"message": "Soll die Gruppe wirklich gelöscht werden?",
|
||||
|
||||
@@ -166,8 +166,7 @@
|
||||
}
|
||||
},
|
||||
"deleteRepo": {
|
||||
"subtitle": "Repository löschen",
|
||||
"button": "Löschen",
|
||||
"button": "Repository löschen",
|
||||
"confirmAlert": {
|
||||
"title": "Repository löschen",
|
||||
"message": "Soll das Repository wirklich gelöscht werden?",
|
||||
@@ -177,7 +176,8 @@
|
||||
},
|
||||
"diff": {
|
||||
"sideBySide": "Zweispaltig",
|
||||
"combined": "Kombiniert"
|
||||
"combined": "Kombiniert",
|
||||
"noDiffFound": "Kein Diff zwischen den ausgewählten Branches gefunden."
|
||||
},
|
||||
"fileUpload": {
|
||||
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",
|
||||
|
||||
@@ -45,8 +45,7 @@
|
||||
"subtitle": "Erstellen eines neuen Benutzers"
|
||||
},
|
||||
"deleteUser": {
|
||||
"subtitle": "Benutzer löschen",
|
||||
"button": "Löschen",
|
||||
"button": "Benutzer löschen",
|
||||
"confirmAlert": {
|
||||
"title": "Benutzer löschen",
|
||||
"message": "Soll der Benutzer wirklich gelöscht werden?",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"communityTitle": "Community Support",
|
||||
"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.",
|
||||
"communityButton": "Contact our team",
|
||||
"communityButton": "Contact our Team",
|
||||
"enterpriseTitle": "Enterprise Support",
|
||||
"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)?",
|
||||
@@ -76,8 +76,8 @@
|
||||
"createSubtitle": "Create Permission Role",
|
||||
"editSubtitle": "Edit Permission Role",
|
||||
"overview": {
|
||||
"title": "Overview of all permission roles",
|
||||
"noPermissionRoles": "No permission roles found.",
|
||||
"title": "Overview of all Permission Roles",
|
||||
"noPermissionRoles": "No Permission Roles found.",
|
||||
"createButton": "Create Permission Role"
|
||||
},
|
||||
"editButton": "Edit",
|
||||
@@ -91,10 +91,9 @@
|
||||
"submit": "Save"
|
||||
},
|
||||
"delete": {
|
||||
"button": "Delete",
|
||||
"subtitle": "Delete permission role",
|
||||
"button": "Delete Permission Role",
|
||||
"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.",
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
|
||||
@@ -60,8 +60,7 @@
|
||||
}
|
||||
},
|
||||
"deleteGroup": {
|
||||
"subtitle": "Delete Group",
|
||||
"button": "Delete",
|
||||
"button": "Delete Group",
|
||||
"confirmAlert": {
|
||||
"title": "Delete Group",
|
||||
"message": "Do you really want to delete the group?",
|
||||
|
||||
@@ -166,8 +166,7 @@
|
||||
}
|
||||
},
|
||||
"deleteRepo": {
|
||||
"subtitle": "Delete Repository",
|
||||
"button": "Delete",
|
||||
"button": "Delete Repository",
|
||||
"confirmAlert": {
|
||||
"title": "Delete repository",
|
||||
"message": "Do you really want to delete the repository?",
|
||||
@@ -184,7 +183,8 @@
|
||||
"copy": "copied"
|
||||
},
|
||||
"sideBySide": "side-by-side",
|
||||
"combined": "combined"
|
||||
"combined": "combined",
|
||||
"noDiffFound": "No Diff between the selected branches found."
|
||||
},
|
||||
"fileUpload": {
|
||||
"clickHere": "Click here to select your file",
|
||||
|
||||
@@ -45,17 +45,16 @@
|
||||
"subtitle": "Create a new user"
|
||||
},
|
||||
"deleteUser": {
|
||||
"subtitle": "Delete User",
|
||||
"button": "Delete",
|
||||
"button": "Delete User",
|
||||
"confirmAlert": {
|
||||
"title": "Delete user",
|
||||
"title": "Delete User",
|
||||
"message": "Do you really want to delete the user?",
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"singleUserPassword": {
|
||||
"button": "Set password",
|
||||
"button": "Set Password",
|
||||
"setPasswordSuccessful": "Password successfully set"
|
||||
},
|
||||
"userForm": {
|
||||
|
||||
@@ -86,8 +86,7 @@
|
||||
"submit": "Guardar"
|
||||
},
|
||||
"delete": {
|
||||
"button": "Borrar",
|
||||
"subtitle": "Eliminar el rol",
|
||||
"button": "Eliminar el rol",
|
||||
"confirmAlert": {
|
||||
"title": "Eliminar el rol",
|
||||
"message": "¿Realmente desea borrar el rol? Todos los usuarios de este rol perderń sus permisos.",
|
||||
|
||||
@@ -60,8 +60,7 @@
|
||||
}
|
||||
},
|
||||
"deleteGroup": {
|
||||
"subtitle": "Borrar grupo",
|
||||
"button": "Borrar",
|
||||
"button": "Borrar grupo",
|
||||
"confirmAlert": {
|
||||
"title": "Borrar grupo",
|
||||
"message": "¿Realmente desea borrar el grupo?",
|
||||
|
||||
@@ -166,8 +166,7 @@
|
||||
}
|
||||
},
|
||||
"deleteRepo": {
|
||||
"subtitle": "Borrar repositorio",
|
||||
"button": "Borrar",
|
||||
"button": "Borrar repositorio",
|
||||
"confirmAlert": {
|
||||
"title": "Borrar repositorio",
|
||||
"message": "¿Realmente desea borrar el repositorio?",
|
||||
@@ -184,7 +183,8 @@
|
||||
"copy": "copiado"
|
||||
},
|
||||
"sideBySide": "dos columnas",
|
||||
"combined": "combinado"
|
||||
"combined": "combinado",
|
||||
"noDiffFound": "No se encontraron diferencias entre las ramas seleccionadas."
|
||||
},
|
||||
"fileUpload": {
|
||||
"clickHere": "Haga click aquí para seleccionar su fichero",
|
||||
|
||||
@@ -45,8 +45,7 @@
|
||||
"subtitle": "Crear un nuevo usuario"
|
||||
},
|
||||
"deleteUser": {
|
||||
"subtitle": "Borrar usuario",
|
||||
"button": "Borrar",
|
||||
"button": "Borrar usuario",
|
||||
"confirmAlert": {
|
||||
"title": "Borrar usuario",
|
||||
"message": "¿Realmente desea borrar el usuario?",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
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 GeneralSettings from "./GeneralSettings";
|
||||
import BaseUrlSettings from "./BaseUrlSettings";
|
||||
@@ -151,11 +151,15 @@ class ConfigForm extends React.Component<Props, State> {
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<Level
|
||||
right={
|
||||
<SubmitButton
|
||||
loading={loading}
|
||||
label={t("config.form.submit")}
|
||||
disabled={!configUpdatePermission || this.hasError() || !this.state.changed}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
|
||||
const waitForRestart = () => {
|
||||
const endTime = Number(new Date()) + 10000;
|
||||
const endTime = Number(new Date()) + 60000;
|
||||
let started = false;
|
||||
|
||||
const executor = (resolve, reject) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user