]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3529 prepare for json parsing of property sets
authorDavid Gageot <david@gageot.net>
Tue, 25 Sep 2012 13:58:42 +0000 (15:58 +0200)
committerDavid Gageot <david@gageot.net>
Tue, 25 Sep 2012 13:59:38 +0000 (15:59 +0200)
19 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
sonar-plugin-api/src/main/java/org/sonar/api/Property.java
sonar-plugin-api/src/main/java/org/sonar/api/config/PropertyDefinition.java
sonar-plugin-api/src/main/java/org/sonar/api/config/PropertySetValue.java
sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java
sonar-plugin-api/src/test/java/org/sonar/api/config/PropertyDefinitionTest.java
sonar-plugin-api/src/test/java/org/sonar/api/config/PropertySetValueTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/config/SettingsTest.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/property_sets_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/settings_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/widget_properties_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/models/property.rb
sonar-server/src/main/webapp/WEB-INF/app/models/property_set.rb
sonar-server/src/main/webapp/WEB-INF/app/views/property_sets/_list.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/property_sets/index.html.erb [deleted file]
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_multi_value.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_properties.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_settings.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/settings/_type_PROPERTY_SET.html.erb

index f5338025e33b4f46f219cf1c349158bd8599e37e..d1647af49946e2fb2a0e3714efcd9b90907f8200 100644 (file)
@@ -63,7 +63,7 @@ import java.util.List;
     name = "Toto",
     global = true,
     type = PropertyType.PROPERTY_SET,
-    property_set_name = "myset",
+    propertySetName = "myset",
     category = CoreProperties.CATEGORY_GENERAL),
   @Property(
     key = CoreProperties.PROJECT_LANGUAGE_PROPERTY,
index 64037142ac218512a8127d8ca5714449eacc1cce..ca2c727069932e2404d9e76aad4d052c4949a1f6 100644 (file)
@@ -104,5 +104,5 @@ public @interface Property {
    *
    * @since 3.3
    */
-  String property_set_name() default "";
+  String propertySetName() default "";
 }
index 5464b2f3cdd4a98a99f1b78397c226d295e7e6a7..5c20b5c56e5b99f55d76dce6dedc0763166ee563 100644 (file)
@@ -67,7 +67,7 @@ public final class PropertyDefinition {
   private boolean onModule = false;
   private boolean isGlobal = true;
   private boolean multiValues;
-  private String property_set_name;
+  private String propertySetName;
 
   private PropertyDefinition(Property annotation) {
     this.key = annotation.key();
@@ -81,7 +81,7 @@ public final class PropertyDefinition {
     this.type = fixType(annotation.key(), annotation.type());
     this.options = annotation.options();
     this.multiValues = annotation.multiValues();
-    this.property_set_name = annotation.property_set_name();
+    this.propertySetName = annotation.propertySetName();
   }
 
   private static PropertyType fixType(String key, PropertyType type) {
@@ -186,7 +186,7 @@ public final class PropertyDefinition {
   /**
    * @since 3.3
    */
-  public String getProperty_set_name() {
-    return property_set_name;
+  public String getPropertySetName() {
+    return propertySetName;
   }
 }
index aadfa06a969fbbac31ff7dc967117bfe77321b3a..8df3ca267cbd2c4f8a1c7f7c439e5edbf8d74171 100644 (file)
  */
 package org.sonar.api.config;
 
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.ArrayUtils;
+import org.sonar.api.utils.DateUtils;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
 /**
  * @since 3.3
  */
-public class PropertySetValue {
+public final class PropertySetValue {
+  private final Map<String, String> keyValues;
+
+  private PropertySetValue(Map<String, String> keyValues) {
+    this.keyValues = ImmutableMap.copyOf(keyValues);
+  }
+
+  public static PropertySetValue create(Map<String, String> keyValues) {
+    return new PropertySetValue(keyValues);
+  }
+
+  /**
+   * @return the field as String. If the field does not exist, then an empty string is returned.
+   */
+  public String getString(String fieldName) {
+    String value = keyValues.get(fieldName);
+    return (value == null) ? "" : value;
+  }
+
+  /**
+   * @return the field as int. If the field does not exist, then <code>0</code> is returned.
+   */
+  public int getInt(String fieldName) {
+    String value = keyValues.get(fieldName);
+    return (value == null) ? 0 : Integer.parseInt(value);
+  }
+
+  /**
+   * @return the field as boolean. If the field does not exist, then <code>false</code> is returned.
+   */
+  public boolean getBoolean(String fieldName) {
+    String value = keyValues.get(fieldName);
+    return (value == null) ? false : Boolean.parseBoolean(value);
+  }
+
+  /**
+   * @return the field as float. If the field does not exist, then <code>0.0</code> is returned.
+   */
+  public float getFloat(String fieldName) {
+    String value = keyValues.get(fieldName);
+    return (value == null) ? 0f : Float.parseFloat(value);
+  }
+
+  /**
+   * @return the field as long. If the field does not exist, then <code>0L</code> is returned.
+   */
+  public long getLong(String fieldName) {
+    String value = keyValues.get(fieldName);
+    return (value == null) ? 0L : Long.parseLong(value);
+  }
+
+  /**
+   * @return the field as Date. If the field does not exist, then <code>null</code> is returned.
+   */
+  public Date getDate(String fieldName) {
+    String value = keyValues.get(fieldName);
+    return (value == null) ? null : DateUtils.parseDate(value);
+  }
+
+  /**
+   * @return the field as Date with time. If the field does not exist, then <code>null</code> is returned.
+   */
+  public Date getDateTime(String fieldName) {
+    String value = keyValues.get(fieldName);
+    return (value == null) ? null : DateUtils.parseDateTime(value);
+  }
+
+  /**
+   * @return the field as an array of String. If the field does not exist, then an empty array is returned.
+   */
+  public String[] getStringArray(String fieldName) {
+    String value = keyValues.get(fieldName);
+    if (value == null) {
+      return ArrayUtils.EMPTY_STRING_ARRAY;
+    }
+
+    List<String> values = Lists.newArrayList();
+    for (String v : Splitter.on(",").trimResults().split(value)) {
+      values.add(v.replace("%2C", ","));
+    }
+    return values.toArray(new String[values.size()]);
+  }
 }
index d9b55af11099fc61a8e43f1e3cbc9be4fe6d3613..0735c91073b470838f04e3ba8ee1837c169c35be 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.api.config;
 
+import com.google.common.collect.Iterables;
+
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
@@ -33,6 +35,8 @@ import org.sonar.api.utils.DateUtils;
 
 import javax.annotation.Nullable;
 import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Project Settings on batch side, Global Settings on server side. This component does not access to database, so
@@ -186,14 +190,17 @@ public class Settings implements BatchComponent, ServerComponent {
       throw new IllegalArgumentException("Property " + key + " is not of type PROPERTY_SET");
     }
 
-    String propertySetValueName = getString(key);
-
-    // read json for given key
-    // search value for given propertySetValueName
+    String propertySetName = property.getPropertySetName();
+    String valueName = getString(key);
+    String propertySetJson = getString("sonar.property_set." + propertySetName);
 
-    return null;
+    return PropertySetValue.create(lowTechJsonParsing(valueName, propertySetJson));
   }
 
+  private static Map<String, String> lowTechJsonParsing(String valueName, String json) {
+    return Maps.newHashMap();
+  }
+  
   /**
    * Value is split by carriage returns.
    *
index 061bb99a575b36c84d7dc0aef9e9712fe1676901..59d3927aa33fdfd335412e5396a59bfd6edd47a5 100644 (file)
@@ -47,7 +47,7 @@ public class PropertyDefinitionTest {
     assertThat(def.isOnProject()).isTrue();
     assertThat(def.isOnModule()).isTrue();
     assertThat(def.isMultiValues()).isTrue();
-    assertThat(def.getProperty_set_name()).isEmpty();
+    assertThat(def.getPropertySetName()).isEmpty();
   }
 
   @Properties(@Property(key = "hello", name = "Hello", defaultValue = "world", description = "desc",
@@ -75,7 +75,7 @@ public class PropertyDefinitionTest {
     assertThat(def.isMultiValues()).isFalse();
   }
 
-  @Properties(@Property(key = "hello", name = "Hello", type = PropertyType.PROPERTY_SET, property_set_name = "set1"))
+  @Properties(@Property(key = "hello", name = "Hello", type = PropertyType.PROPERTY_SET, propertySetName = "set1"))
   static class WithPropertySet {
   }
 
@@ -87,7 +87,7 @@ public class PropertyDefinitionTest {
     PropertyDefinition def = PropertyDefinition.create(prop);
 
     assertThat(def.getType()).isEqualTo(PropertyType.PROPERTY_SET);
-    assertThat(def.getProperty_set_name()).isEqualTo("set1");
+    assertThat(def.getPropertySetName()).isEqualTo("set1");
   }
 
   @Properties(@Property(key = "hello", name = "Hello"))
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertySetValueTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/PropertySetValueTest.java
new file mode 100644 (file)
index 0000000..b59ac26
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.config;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class PropertySetValueTest {
+  @Test
+  public void should_get_default_values() {
+    PropertySetValue value = PropertySetValue.create(Maps.<String, String>newHashMap());
+
+    assertThat(value.getString("UNKNOWN")).isEmpty();
+    assertThat(value.getInt("UNKNOWN")).isZero();
+    assertThat(value.getFloat("UNKNOWN")).isZero();
+    assertThat(value.getLong("UNKNOWN")).isZero();
+    assertThat(value.getDate("UNKNOWN")).isNull();
+    assertThat(value.getDateTime("UNKNOWN")).isNull();
+    assertThat(value.getStringArray("UNKNOWN")).isEmpty();
+  }
+
+  @Test
+  public void should_get_values() {
+    PropertySetValue value = PropertySetValue.create(ImmutableMap.<String, String>builder()
+      .put("age", "12")
+      .put("child", "true")
+      .put("size", "12.4")
+      .put("distance", "1000000000")
+      .put("array", "1,2,3,4,5")
+      .put("birth", "1975-01-29")
+      .put("now", "2012-09-25T10:08:30+0100")
+      .build());
+
+    assertThat(value.getString("age")).isEqualTo("12");
+    assertThat(value.getInt("age")).isEqualTo(12);
+    assertThat(value.getBoolean("child")).isTrue();
+    assertThat(value.getFloat("size")).isEqualTo(12.4f);
+    assertThat(value.getLong("distance")).isEqualTo(1000000000L);
+    assertThat(value.getStringArray("array")).contains("1", "2", "3", "4", "5");
+    assertThat(value.getDate("birth")).isNotNull();
+    assertThat(value.getDateTime("now")).isNotNull();
+  }
+}
index cef3a3fbdfb3915b7e3553b4abfc6db1c9c5fd9f..137e76a8b4fc48d1e4424ade3ccbdbadfc8e983a 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.api.config;
 
+import org.junit.Ignore;
+
 import com.google.common.collect.ImmutableMap;
 import org.junit.Before;
 import org.junit.Rule;
@@ -26,6 +28,7 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.Properties;
 import org.sonar.api.Property;
+import org.sonar.api.PropertyType;
 
 import static org.fest.assertions.Assertions.assertThat;
 
@@ -41,7 +44,8 @@ public class SettingsTest {
     @Property(key = "falseboolean", name = "False Boolean", defaultValue = "false"),
     @Property(key = "integer", name = "Integer", defaultValue = "12345"),
     @Property(key = "array", name = "Array", defaultValue = "one,two,three"),
-    @Property(key = "multi_values", name = "Array", defaultValue = "1,2,3", multiValues = true)
+    @Property(key = "multi_values", name = "Array", defaultValue = "1,2,3", multiValues = true),
+    @Property(key = "sonar.jira", name = "Jira Server", type = PropertyType.PROPERTY_SET, propertySetName = "jira")
   })
   static class Init {
   }
@@ -157,47 +161,47 @@ public class SettingsTest {
   @Test
   public void setStringArray() {
     Settings settings = new Settings(definitions);
-    settings.setProperty("multi_values", new String[] {"A", "B"});
+    settings.setProperty("multi_values", new String[]{"A", "B"});
     String[] array = settings.getStringArray("multi_values");
-    assertThat(array).isEqualTo(new String[] {"A", "B"});
+    assertThat(array).isEqualTo(new String[]{"A", "B"});
   }
 
   @Test
   public void setStringArrayTrimValues() {
     Settings settings = new Settings(definitions);
-    settings.setProperty("multi_values", new String[] {" A ", " B "});
+    settings.setProperty("multi_values", new String[]{" A ", " B "});
     String[] array = settings.getStringArray("multi_values");
-    assertThat(array).isEqualTo(new String[] {"A", "B"});
+    assertThat(array).isEqualTo(new String[]{"A", "B"});
   }
 
   @Test
   public void setStringArrayEscapeCommas() {
     Settings settings = new Settings(definitions);
-    settings.setProperty("multi_values", new String[] {"A,B", "C,D"});
+    settings.setProperty("multi_values", new String[]{"A,B", "C,D"});
     String[] array = settings.getStringArray("multi_values");
-    assertThat(array).isEqualTo(new String[] {"A,B", "C,D"});
+    assertThat(array).isEqualTo(new String[]{"A,B", "C,D"});
   }
 
   @Test
   public void setStringArrayWithEmptyValues() {
     Settings settings = new Settings(definitions);
-    settings.setProperty("multi_values", new String[] {"A,B", "", "C,D"});
+    settings.setProperty("multi_values", new String[]{"A,B", "", "C,D"});
     String[] array = settings.getStringArray("multi_values");
-    assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"});
+    assertThat(array).isEqualTo(new String[]{"A,B", "", "C,D"});
   }
 
   @Test
   public void setStringArrayWithNullValues() {
     Settings settings = new Settings(definitions);
-    settings.setProperty("multi_values", new String[] {"A,B", null, "C,D"});
+    settings.setProperty("multi_values", new String[]{"A,B", null, "C,D"});
     String[] array = settings.getStringArray("multi_values");
-    assertThat(array).isEqualTo(new String[] {"A,B", "", "C,D"});
+    assertThat(array).isEqualTo(new String[]{"A,B", "", "C,D"});
   }
 
   @Test(expected = IllegalStateException.class)
   public void shouldFailToSetArrayValueOnSingleValueProperty() {
     Settings settings = new Settings(definitions);
-    settings.setProperty("array", new String[] {"A", "B", "C"});
+    settings.setProperty("array", new String[]{"A", "B", "C"});
   }
 
   @Test
@@ -319,4 +323,19 @@ public class SettingsTest {
     assertThat(settings.getKeysStartingWith("sonar.jdbc")).containsOnly("sonar.jdbc.url", "sonar.jdbc.username");
     assertThat(settings.getKeysStartingWith("other")).hasSize(0);
   }
+
+  @Test
+  @Ignore
+  public void should_get_property_set_value() {
+    Settings settings = new Settings(definitions);
+    settings.setProperty("sonar.property_set.jira",
+        "[{\"set\": {\"name\": \"codehaus_jira\", \"values\": {\"key1\":\"value1\", \"key2\":\"value2\"}}},{\"set\": {\"name\": \"other\", \"values\": {\"key3\":\"value3\"}}}]");
+
+    settings.setProperty("sonar.jira", "codehaus_jira");
+    assertThat(settings.getPropertySetValue("sonar.jira").getString("key1")).isEqualTo("value1");
+    assertThat(settings.getPropertySetValue("sonar.jira").getString("key2")).isEqualTo("value2");
+
+    settings.setProperty("sonar.jira", "other");
+    assertThat(settings.getPropertySetValue("sonar.jira").getString("key3")).isEqualTo("value3");
+  }
 }
index 185a439e7583b90dde664fda8de173a8fcf29b38..7d53ee47b7ed4476818358fc138213f7e53b8a34 100644 (file)
@@ -24,6 +24,7 @@ class PropertySetsController < ApplicationController
 
   def index
     @property_sets = java_facade.listPropertySets()
+    render :partial => 'property_sets/list'
   end
 
 end
index d64fd1d3744cca7a6edaf1db2bc3a60f525136d3..4ff3b687e35e5ce36aaff2941cd7e48b49bfcced 100644 (file)
@@ -51,14 +51,15 @@ module SettingsHelper
   end
 
   def by_name(categories)
-    categories.sort_by { |category| category_name(category) }
+    Api::Utils.insensitive_sort(categories) { |category| category_name(category) }
   end
 
   def input_name(property)
     h(property.key) + (property.multi_values ? '[]' : '')
   end
 
-  def property_set_values(property)
-    PropertySet.findAll(property.property_set_name);
+  def property_set_value_names(property)
+    names = PropertySet.findAll(property.propertySetName).map(&:name);
+    Api::Utils.insensitive_sort(names)
   end
 end
index 1f9841e809e2a800282f7d04fb1130db7a75f07b..c6a3dda51492e33dc301402efc8cdebb40076ddc 100644 (file)
@@ -24,5 +24,4 @@ module WidgetPropertiesHelper
     property_value definition.key(), definition.type.name(), value.nil? ? definition.defaultValue() : value
   end
 
-
 end
index f9e0e4e9fbf3b37aa7773ab60ebc1f2fe7fdf6b2..976fdf7480a8909398da5f70223b9725b87502c6 100644 (file)
@@ -23,6 +23,7 @@ class Property < ActiveRecord::Base
   named_scope :with_key, lambda { |value| {:conditions => {:prop_key, value}} }
   named_scope :with_resource, lambda { |value| {:conditions => {:resource_id => value}} }
   named_scope :with_user, lambda { |value| {:conditions => {:user_id => value}} }
+  named_scope :on_resource, :conditions => ['resource_id is not ?', nil]
 
   def key
     prop_key
index b7e20fe1a1e5d3796551998181137e8f259cb0ac..1defe5dece2d2603e01564ac49fbe6a84bd18ad1 100644 (file)
@@ -42,6 +42,6 @@ class PropertySet < ActiveRecord::Base
     json = Property.value('sonar.property_set.' + set_name)
 
     #json || '[]'
-    json || '[{"name":"set1"},{"name":"set2"}]'
+    json || '[{"name":"set2"},{"name":"set1"}]'
   end
 end
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/property_sets/_list.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/property_sets/_list.html.erb
new file mode 100644 (file)
index 0000000..a09bcb1
--- /dev/null
@@ -0,0 +1,22 @@
+<form id="edit-property-set-form" method="post" action="property-sets/update">
+  <fieldset>
+    <div class="form-head">
+    </div>
+
+    <div class="form-body">
+      <% @property_sets.each do |property_set| -%>
+        <% property_set.fields.each do |field| -%>
+          <%= field.name %> (<%= field.type %>)<br/>
+        <% end -%>
+      <% end -%>
+    </div>
+
+    <div class="form-foot">
+      <a href="#" onclick="return closeModalWindow();" id="rename-cancel"><%= h message('cancel') -%></a>
+    </div>
+  </fieldset>
+</form>
+
+<script>
+  $j("#edit-property-set-form").modalForm();
+</script>
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/property_sets/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/property_sets/index.html.erb
deleted file mode 100644 (file)
index 37f9e57..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<% @property_sets.each do |property_set| -%>
-  <% property_set.fields.each do |field| -%>
-    <%= field.name %>
-    <%= field.type %>
-  <% end -%>
-<% end -%>
\ No newline at end of file
index 3bc94037b05b0a2dacf1e00bfc0e90e982910836..b98f736a8d02f7b64f87d81a24aca8f080be02fc 100644 (file)
@@ -1,5 +1,7 @@
 <div class="multi_value marginbottom5">
   <%= render "settings/type_#{property_type(property, value)}", :property => property, :value => value -%>
-  <a href="#" class="delete link-action"><%= message('delete') -%></a>
+  <% if delete_link -%>
+    <a href="#" class="delete link-action"><%= message('delete') -%></a>
+  <% end -%>
   <br/>
 </div>
index f671a1ff32ee843b387f30ed671fdd9098797e20..458b02c27c295f394daa77427a23084572704c00 100644 (file)
 
             <% value = property_value(property) -%>
             <% if property.multi_values -%>
-              <% value.each do |sub_value| -%>
-                <%= render "settings/multi_value", :property => property, :value => sub_value -%>
+              <% value.each_with_index do |sub_value, index| -%>
+                <%= render "settings/multi_value", :property => property, :value => sub_value, :delete_link => true -%>
               <% end -%>
               <div class="template" style="display:none;">
-                <%= render "settings/multi_value", :property => property, :value => nil -%>
+                <%= render "settings/multi_value", :property => property, :value => nil, :delete_link => true -%>
               </div>
               <button class="add_value"><%= message('settings.add') -%></button>
               <br/>
index b4a829782ef468b68972cdaa74b23dd0fc584946..347811ce84128cf5e5338a956666f98d95413134 100644 (file)
@@ -1,5 +1,5 @@
 <div id="plugins">
-  <h1 class="marginbottom10"><%= message('settings.page') -%></h1>
+  <h1 class="marginbottom10"><%= message(@resource ? 'project_settings.page' : 'settings.page' ) -%></h1>
 
   <table width="100%">
     <tr>
index 613c806d5815be80b9db329ca5db5d42951200d3..aa9a1bbb2a04f6fb7063b454136c6520dadecc26 100644 (file)
@@ -1,7 +1,7 @@
 <select name="<%= input_name(property) -%>" id="input_<%= h property.key -%>">
   <option value=""><%= message('default') -%></option>
-  <% property_set_values(property).map(&:name).each do |option| %>
+  <% property_set_value_names(property).each do |option| %>
     <option value="<%= h option -%>" <%= 'selected' if value && value==option -%>><%= h option -%></option>
   <% end %>
-  <option value="">New value...</option>
 </select>
+<a id="edit-set-<%= h property.key -%>" href="property_sets/index" class="open-modal link-action">Edit property set...</a>