Browse Source

HUE-3872 [core] Add options combobox to properties that have options defined

This also introduces selectize.js as a replacement for select2
Johan Ahlen 9 năm trước cách đây
mục cha
commit
15e0a0d

+ 1 - 0
apps/useradmin/src/useradmin/static/useradmin/css/useradmin.css

@@ -58,6 +58,7 @@ input[type=submit] {
 
 @media all and (max-height: 800px) {
   .form-actions {
+    z-index: 2;
     border-top: 1px solid #F1F1F1;
     position: fixed;
     bottom: 0;

+ 3 - 3
apps/useradmin/src/useradmin/templates/list_configurations.mako

@@ -126,7 +126,7 @@ ${layout.menubar(section='configurations')}
 
 <div class="container-fluid">
   <!-- ko hueSpinner: { spin: loading, center: true, size: 'large' } --><!-- /ko -->
-  <h4 style="width: 100%; text-align: center;" data-bind="visible: !loading() && hasErrors()">${ _('There was an error loading the configurations') }</h4>
+  <h4 style="width: 100%; text-align: center; display: none;" data-bind="visible: !loading() && hasErrors()">${ _('There was an error loading the configurations') }</h4>
   <!-- ko template: { if: !loading() && !hasErrors() && !selectedApp(), name: 'app-list' } --><!-- /ko -->
   <!-- ko template: { if: !loading() && !hasErrors() && selectedApp(), name: 'edit-app' } --><!-- /ko -->
 </div>
@@ -268,10 +268,10 @@ ${ configKoComponents.config() }
 
         // Merge base properties with default properties into default
         app.default.forEach(function (defaultProperty) {
-          defaultIndex[defaultProperty.name] = defaultProperty;
+          defaultIndex[defaultProperty.name || defaultProperty.nice_name] = defaultProperty;
         });
         app.properties.forEach(function (property) {
-          if (!defaultIndex[property.name]) {
+          if (!defaultIndex[property.name || property.nice_name]) {
             app.default.push(property);
           }
         });

+ 314 - 0
desktop/core/src/desktop/static/desktop/ext/css/selectize.css

@@ -0,0 +1,314 @@
+/**
+ * selectize.css (v0.12.1)
+ * Copyright (c) 2013–2015 Brian Reavis & contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+ * file except in compliance with the License. You may obtain a copy of the License at:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+ * ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ *
+ * @author Brian Reavis <brian@thirdroute.com>
+ */
+
+.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
+  visibility: visible !important;
+  background: #f2f2f2 !important;
+  background: rgba(0, 0, 0, 0.06) !important;
+  border: 0 none !important;
+  -webkit-box-shadow: inset 0 0 12px 4px #ffffff;
+  box-shadow: inset 0 0 12px 4px #ffffff;
+}
+.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
+  content: '!';
+  visibility: hidden;
+}
+.selectize-control.plugin-drag_drop .ui-sortable-helper {
+  -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+}
+.selectize-dropdown-header {
+  position: relative;
+  padding: 5px 8px;
+  border-bottom: 1px solid #d0d0d0;
+  background: #f8f8f8;
+  -webkit-border-radius: 3px 3px 0 0;
+  -moz-border-radius: 3px 3px 0 0;
+  border-radius: 3px 3px 0 0;
+}
+.selectize-dropdown-header-close {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  color: #303030;
+  opacity: 0.4;
+  margin-top: -12px;
+  line-height: 20px;
+  font-size: 20px !important;
+}
+.selectize-dropdown-header-close:hover {
+  color: #000000;
+}
+.selectize-dropdown.plugin-optgroup_columns .optgroup {
+  border-right: 1px solid #f2f2f2;
+  border-top: 0 none;
+  float: left;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
+  border-right: 0 none;
+}
+.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
+  display: none;
+}
+.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
+  border-top: 0 none;
+}
+.selectize-control.plugin-remove_button [data-value] {
+  position: relative;
+  padding-right: 24px !important;
+}
+.selectize-control.plugin-remove_button [data-value] .remove {
+  z-index: 1;
+  /* fixes ie bug (see #392) */
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  width: 17px;
+  text-align: center;
+  font-weight: bold;
+  font-size: 12px;
+  color: inherit;
+  text-decoration: none;
+  vertical-align: middle;
+  display: inline-block;
+  padding: 2px 0 0 0;
+  border-left: 1px solid #d0d0d0;
+  -webkit-border-radius: 0 2px 2px 0;
+  -moz-border-radius: 0 2px 2px 0;
+  border-radius: 0 2px 2px 0;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.selectize-control.plugin-remove_button [data-value] .remove:hover {
+  background: rgba(0, 0, 0, 0.05);
+}
+.selectize-control.plugin-remove_button [data-value].active .remove {
+  border-left-color: #cacaca;
+}
+.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {
+  background: none;
+}
+.selectize-control.plugin-remove_button .disabled [data-value] .remove {
+  border-left-color: #ffffff;
+}
+.selectize-control {
+  position: relative;
+}
+.selectize-dropdown,
+.selectize-input,
+.selectize-input input {
+  color: #303030;
+  font-family: inherit;
+  font-size: 13px;
+  line-height: 18px;
+  -webkit-font-smoothing: inherit;
+}
+.selectize-input,
+.selectize-control.single .selectize-input.input-active {
+  background: #ffffff;
+  cursor: text;
+  display: inline-block;
+}
+.selectize-input {
+  border: 1px solid #d0d0d0;
+  padding: 8px 8px;
+  display: inline-block;
+  width: 100%;
+  overflow: hidden;
+  position: relative;
+  z-index: 1;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1);
+}
+.selectize-control.multi .selectize-input.has-items {
+  padding: 6px 8px 3px;
+}
+.selectize-input.full {
+  background-color: #ffffff;
+}
+.selectize-input.disabled,
+.selectize-input.disabled * {
+  cursor: default !important;
+}
+.selectize-input.focus {
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
+}
+.selectize-input.dropdown-active {
+  -webkit-border-radius: 3px 3px 0 0;
+  -moz-border-radius: 3px 3px 0 0;
+  border-radius: 3px 3px 0 0;
+}
+.selectize-input > * {
+  vertical-align: baseline;
+  display: -moz-inline-stack;
+  display: inline-block;
+  zoom: 1;
+  *display: inline;
+}
+.selectize-control.multi .selectize-input > div {
+  cursor: pointer;
+  margin: 0 3px 3px 0;
+  padding: 2px 6px;
+  background: #f2f2f2;
+  color: #303030;
+  border: 0 solid #d0d0d0;
+}
+.selectize-control.multi .selectize-input > div.active {
+  background: #e8e8e8;
+  color: #303030;
+  border: 0 solid #cacaca;
+}
+.selectize-control.multi .selectize-input.disabled > div,
+.selectize-control.multi .selectize-input.disabled > div.active {
+  color: #7d7d7d;
+  background: #ffffff;
+  border: 0 solid #ffffff;
+}
+.selectize-input > input {
+  display: inline-block !important;
+  padding: 0 !important;
+  min-height: 0 !important;
+  max-height: none !important;
+  max-width: 100% !important;
+  margin: 0 2px 0 0 !important;
+  text-indent: 0 !important;
+  border: 0 none !important;
+  background: none !important;
+  line-height: inherit !important;
+  -webkit-user-select: auto !important;
+  -webkit-box-shadow: none !important;
+  box-shadow: none !important;
+}
+.selectize-input > input::-ms-clear {
+  display: none;
+}
+.selectize-input > input:focus {
+  outline: none !important;
+}
+.selectize-input::after {
+  content: ' ';
+  display: block;
+  clear: left;
+}
+.selectize-input.dropdown-active::before {
+  content: ' ';
+  display: block;
+  position: absolute;
+  background: #f0f0f0;
+  height: 1px;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+.selectize-dropdown {
+  position: absolute;
+  z-index: 10;
+  border: 1px solid #d0d0d0;
+  background: #ffffff;
+  margin: -1px 0 0 0;
+  border-top: 0 none;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  -webkit-border-radius: 0 0 3px 3px;
+  -moz-border-radius: 0 0 3px 3px;
+  border-radius: 0 0 3px 3px;
+}
+.selectize-dropdown [data-selectable] {
+  cursor: pointer;
+  overflow: hidden;
+}
+.selectize-dropdown [data-selectable] .highlight {
+  background: rgba(125, 168, 208, 0.2);
+  -webkit-border-radius: 1px;
+  -moz-border-radius: 1px;
+  border-radius: 1px;
+}
+.selectize-dropdown [data-selectable],
+.selectize-dropdown .optgroup-header {
+  padding: 5px 8px;
+}
+.selectize-dropdown .optgroup:first-child .optgroup-header {
+  border-top: 0 none;
+}
+.selectize-dropdown .optgroup-header {
+  color: #303030;
+  background: #ffffff;
+  cursor: default;
+}
+.selectize-dropdown .active {
+  background-color: #f5fafd;
+  color: #495c68;
+}
+.selectize-dropdown .active.create {
+  color: #495c68;
+}
+.selectize-dropdown .create {
+  color: rgba(48, 48, 48, 0.5);
+}
+.selectize-dropdown-content {
+  overflow-y: auto;
+  overflow-x: hidden;
+  max-height: 200px;
+}
+.selectize-control.single .selectize-input,
+.selectize-control.single .selectize-input input {
+  cursor: pointer;
+}
+.selectize-control.single .selectize-input.input-active,
+.selectize-control.single .selectize-input.input-active input {
+  cursor: text;
+}
+.selectize-control.single .selectize-input:after {
+  content: ' ';
+  display: block;
+  position: absolute;
+  top: 50%;
+  right: 15px;
+  margin-top: -3px;
+  width: 0;
+  height: 0;
+  border-style: solid;
+  border-width: 5px 5px 0 5px;
+  border-color: #808080 transparent transparent transparent;
+}
+.selectize-control.single .selectize-input.dropdown-active:after {
+  margin-top: -4px;
+  border-width: 0 5px 5px 5px;
+  border-color: transparent transparent #808080 transparent;
+}
+.selectize-control.rtl.single .selectize-input:after {
+  left: 15px;
+  right: auto;
+}
+.selectize-control.rtl .selectize-input > input {
+  margin: 0 4px 0 -2px !important;
+}
+.selectize-control .selectize-input.disabled {
+  opacity: 0.5;
+  background-color: #fafafa;
+}

+ 132 - 0
desktop/core/src/desktop/static/desktop/ext/js/knockout-selectize.js

@@ -0,0 +1,132 @@
+// Based on source from:
+
+(function (factory) {
+  if(typeof define === "function") {
+    define("ko.selectize", ["knockout", "selectize"], factory);
+  } else {
+    factory(ko, selectize);
+  }
+}(function (ko) {
+  var inject_binding = function (allBindings, key, value) {
+    return {
+      has: function (bindingKey) {
+        return (bindingKey == key) || allBindings.has(bindingKey);
+      },
+      get: function (bindingKey) {
+        var binding = allBindings.get(bindingKey);
+        if (bindingKey == key) {
+          binding = binding ? [].concat(binding, value) : value;
+        }
+        return binding;
+      }
+    };
+  };
+
+  ko.bindingHandlers.selectize = {
+    after: ['options'],
+    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+      if (!allBindingsAccessor.has('optionsText'))
+        allBindingsAccessor = inject_binding(allBindingsAccessor, 'optionsText', 'name');
+      if (!allBindingsAccessor.has('optionsValue'))
+        allBindingsAccessor = inject_binding(allBindingsAccessor, 'optionsValue', 'id');
+
+      ko.bindingHandlers.options.update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
+
+      var options = {
+        valueField: allBindingsAccessor.get('optionsValue'),
+        labelField: allBindingsAccessor.get('optionsText'),
+        searchField: allBindingsAccessor.get('optionsText'),
+        create: true,
+        createOnBlur: true,
+        allowEmptyOption: true
+      };
+
+      if (allBindingsAccessor.has('options')) {
+        var passed_options = allBindingsAccessor.get('options');
+        for (var attr_name in passed_options) {
+          options[attr_name] = passed_options[attr_name];
+        }
+      }
+
+      var $select = $(element).selectize(options)[0].selectize;
+
+      if (typeof allBindingsAccessor.get('value') == 'function') {
+        $select.addItem(allBindingsAccessor.get('value')());
+        allBindingsAccessor.get('value').subscribe(function (new_val) {
+          $select.addItem(new_val);
+        })
+      }
+
+      if (typeof allBindingsAccessor.get('selectedOptions') == 'function') {
+        allBindingsAccessor.get('selectedOptions').subscribe(function (new_val) {
+          // Removing items which are not in new value
+          var values = $select.getValue();
+          var items_to_remove = [];
+          for (var k in values) {
+            if (new_val.indexOf(values[k]) == -1) {
+              items_to_remove.push(values[k]);
+            }
+          }
+
+          for (var k in items_to_remove) {
+            $select.removeItem(items_to_remove[k]);
+          }
+
+          for (var k in new_val) {
+            $select.addItem(new_val[k]);
+          }
+
+        });
+        var selected = allBindingsAccessor.get('selectedOptions')();
+        for (var k in selected) {
+          $select.addItem(selected[k]);
+        }
+      }
+
+
+      if (typeof init_selectize == 'function') {
+        init_selectize($select);
+      }
+
+      if (typeof valueAccessor().subscribe == 'function') {
+        valueAccessor().subscribe(function (changes) {
+          // To avoid having duplicate keys, all delete operations will go first
+          var addedItems = new Array();
+          changes.forEach(function (change) {
+            switch (change.status) {
+              case 'added':
+                addedItems.push(change.value);
+                break;
+              case 'deleted':
+                var itemId = change.value[options.valueField];
+                if (itemId != null) $select.removeOption(itemId);
+            }
+          });
+          addedItems.forEach(function (item) {
+            $select.addOption(item);
+          });
+
+        }, null, "arrayChange");
+      }
+
+    },
+    update: function (element, valueAccessor, allBindingsAccessor) {
+
+      if (allBindingsAccessor.has('object')) {
+        var optionsValue = allBindingsAccessor.get('optionsValue') || 'id';
+        var value_accessor = valueAccessor();
+        var selected_obj = $.grep(value_accessor(), function (i) {
+          if (typeof i[optionsValue] == 'function')
+            var id = i[optionsValue];
+          else
+            var id = i[optionsValue];
+          return id == allBindingsAccessor.get('value')();
+        })[0];
+
+        if (selected_obj) {
+          allBindingsAccessor.get('object')(selected_obj);
+        }
+      }
+    }
+  }
+}));

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
desktop/core/src/desktop/static/desktop/ext/js/selectize.min.js


+ 49 - 11
desktop/core/src/desktop/templates/config_ko_components.mako

@@ -23,9 +23,10 @@ from desktop.views import _ko
 
 <%def name="config()">
 
+  <link rel="stylesheet" href="${ static('desktop/ext/css/selectize.css') }">
+
   <style>
     .config-property {
-      display: inline-block;
       vertical-align: top;
       margin-bottom: 20px;
       position: relative;
@@ -37,7 +38,7 @@ from desktop.views import _ko
 
     .config-label {
       display: inline-block;
-      min-width: 100px;
+      min-width: 130px;
       margin: 4px 10px;
       float:left;
       text-align: right;
@@ -62,6 +63,17 @@ from desktop.views import _ko
       font-size: 14px;
       color: #888;
     }
+
+    .selectize-wrapper {
+      display: inline-block;
+      float:left;
+      margin-right: 4px;
+    }
+
+    .selectize-input {
+      padding-top: 6px !important;
+      padding-bottom: 5px !important;
+    }
   </style>
 
   <script type="text/html" id="property-selector-template">
@@ -73,11 +85,12 @@ from desktop.views import _ko
         label: nice_name,
         helpText: help_text,
         value: value,
+        property: $data,
         visibleObservable: $parent.visibleObservable
       }
     } --><!-- /ko -->
     <!-- /ko -->
-    <div class="config-property margin-left-10" data-bind="visible: availableProperties().length > 0">
+    <div class="margin-left-10" data-bind="visible: availableProperties().length > 0">
       <select data-bind="options: availableProperties, optionsText: 'nice_name', optionsCaption: '${_ko('Add a property...')}', value: propertyToAdd"></select>
       <div style="display: inline-block; vertical-align: top; margin-top: 6px; margin-left: 6px;">
         <a class="inactive-action pointer" data-bind="click: addProperty">
@@ -209,7 +222,7 @@ from desktop.views import _ko
   <script type="text/javascript" charset="utf-8">
     (function (factory) {
       if (typeof require === "function") {
-        require(['knockout'], factory);
+        require(['knockout', 'ko.selectize'], factory);
       } else {
         factory(ko);
       }
@@ -290,7 +303,14 @@ from desktop.views import _ko
   </script>
 
   <script type="text/html" id="property-string">
-    <input class="input-small" type="text" data-bind="textInput: value, valueUpdate:'afterkeydown'" />
+    <!-- ko if: typeof property !== 'undefined' && typeof property.options !== 'undefined' && property.options().length > 0 -->
+    <div class="selectize-wrapper" style="min-width: 200px;">
+      <select placeholder="${ _('Key') }" data-bind="selectize: $.map($parent.options(), function (option) { return { value: option } }), value: value, options: $.map($parent.options(), function (option) { return { value: option } }), optionsText: 'value', optionsValue: 'value'"></select>
+    </div>
+    <!-- /ko -->
+    <!-- ko if: typeof property === 'undefined' || typeof property.options() === 'undefined' || property.options().length === 0 -->
+    <input class="input" type="text" data-bind="textInput: value, valueUpdate:'afterkeydown'" />
+    <!-- /ko -->
   </script>
 
   <script type="text/html" id="property-boolean">
@@ -302,7 +322,7 @@ from desktop.views import _ko
   </script>
 
   <script type="text/html" id="property-settings">
-    <div data-bind="component: { name: 'key-value-list-input', params: { values: value, visibleObservable: visibleObservable } }"></div>
+    <div data-bind="component: { name: 'key-value-list-input', params: { values: value, visibleObservable: visibleObservable, property: property } }"></div>
   </script>
 
   <script type="text/html" id="property-hdfs-files">
@@ -328,7 +348,7 @@ from desktop.views import _ko
 
   <script type="text/html" id="property-hdfs-file">
     <div class="input-append">
-      <input type="text" class="filechooser-input" data-bind="value: value, valueUpdate:'afterkeydown', filechooser: { value: value, isAddon: true}" placeholder="${ _('Path to the file, e.g. hdfs://localhost:8020/user/hue') }"/>
+      <input type="text" style="min-width: 300px" class="filechooser-input" data-bind="value: value, valueUpdate:'afterkeydown', filechooser: { value: value, isAddon: true}" placeholder="${ _('Path to the file, e.g. hdfs://localhost:8020/user/hue') }"/>
     </div>
   </script>
 
@@ -386,10 +406,17 @@ from desktop.views import _ko
 
   <script type="text/html" id="key-value-list-input-template">
     <ul data-bind="sortable: { data: values, options: { axis: 'y', containment: 'parent' }}, visible: values().length" class="unstyled">
-      <li>
+      <li style="clear:both;">
+        <!-- ko if: $parent.options.length > 0 -->
+        <div class="selectize-wrapper" style="min-width: 200px;">
+          <select placeholder="${ _('Key') }" data-bind="selectize: $parent.options, value: key, options: $parent.options, optionsText: 'value', optionsValue: 'value'"></select>
+        </div>
+        <!-- /ko -->
         <div class="input-append" style="margin-bottom: 4px">
-          <input type="text" style="width: 130px" placeholder="${ _('Key') }" data-bind="textInput: key, valueUpdate: 'afterkeydown'"/>
-          <input type="text" style="width: 130px" placeholder="${ _('Value') }" data-bind="textInput: value, valueUpdate: 'afterkeydown'"/>
+          <!-- ko if: $parent.options.length === 0 -->
+          <input type="text" style="width: 182px; margin-right: 4px;" placeholder="${ _('Key') }" data-bind="textInput: key, valueUpdate: 'afterkeydown'"/>
+          <!-- /ko -->
+          <input type="text" style="width: 182px;" placeholder="${ _('Value') }" data-bind="textInput: value, valueUpdate: 'afterkeydown'"/>
           <span class="add-on move-widget muted"><i class="fa fa-arrows"></i></span>
           <a class="add-on muted" href="javascript: void(0);" data-bind="click: function(){ $parent.removeValue($data); }"><i class="fa fa-minus"></i></a>
         </div>
@@ -415,6 +442,17 @@ from desktop.views import _ko
         function KeyValueListInputViewModel(params) {
           var self = this;
           self.values = params.values;
+          self.options = typeof params.property.options !== 'undefined' ? $.map(params.property.options(), function (option) {
+            return { value: option }
+          }) : [];
+
+          if (self.options.length > 0) {
+            self.values().forEach(function (value) {
+              if (self.options.indexOf(value.key()) === -1) {
+                self.options.push({ value: value.key() });
+              }
+            })
+          }
           params.visibleObservable.subscribe(function (newValue) {
             if (!newValue) {
               self.values($.grep(self.values(), function (value) {
@@ -450,7 +488,7 @@ from desktop.views import _ko
     <ul data-bind="sortable: { data: values, options: { axis: 'y', containment: 'parent' }}, visible: values().length" class="unstyled">
       <li>
         <div class="input-append" style="margin-bottom: 4px">
-          <input type="text" style="width: 110px" placeholder="${ _('Name, e.g. foo') }" data-bind="textInput: name, valueUpdate: 'afterkeydown'"/>
+          <input type="text" style="width: 182px; margin-right: 4px;" placeholder="${ _('Name, e.g. foo') }" data-bind="textInput: name, valueUpdate: 'afterkeydown'"/>
           <input type="text" style="width: 150px" placeholder="${ _('Class, e.g. org.hue.Bar') }" data-bind="textInput: class_name, valueUpdate: 'afterkeydown'"/>
           <span class="add-on move-widget muted"><i class="fa fa-arrows"></i></span>
           <a class="add-on muted" href="javascript: void(0);" data-bind="click: function(){ $parent.removeValue($data); }"><i class="fa fa-minus"></i></a>

+ 3 - 1
desktop/core/src/desktop/templates/require.mako

@@ -41,7 +41,9 @@ DEBUG = DJANGO_DEBUG_MODE.get()
         "knockout-sortable" : "desktop/ext/js/knockout-sortable.min",
         "ko.editable" : "desktop/js/ko.editable",
         "ko.switch-case" : "desktop/js/ko.switch-case",
-        "ko.hue-bindings" : "desktop/js/ko.hue-bindings"
+        "ko.hue-bindings" : "desktop/js/ko.hue-bindings",
+        "selectize" : "desktop/ext/js/selectize.min",
+        "ko.selectize" : "desktop/ext/js/knockout-selectize"
       },
       shim: {
         "knockout": { exports: "ko" },

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác