瀏覽代碼

HUE-1111 [beeswax] Save and edit the description of a query

Introducing X-editable
Ajax edition of Name and Description
Checks for empty name
Add tests
Romain Rigaux 12 年之前
父節點
當前提交
ea8b714d5c

+ 7 - 1
NOTICE.txt

@@ -5,4 +5,10 @@ This product includes software developed by
 Andrew Valums (http://valums.com/ajax-upload/).
 
 This product includes software developed by
-Anders Fajerson (https://github.com/perifer/timePicker)
+Anders Fajerson (https://github.com/perifer/timePicker)
+
+This product includes software developed by
+Vitaliy Potapov (http://vitalets.github.com/x-editable/)
+
+This product includes software developed by
+Prasanthj (https://github.com/prasanthj/pig-codemirror-2)

+ 5 - 3
apps/beeswax/src/beeswax/forms.py

@@ -59,14 +59,16 @@ class SaveForm(forms.Form):
     return self.cleaned_data.get('name', '').strip()
 
   def clean(self):
+    cleaned_data = super(SaveForm, self).clean()
+
     if self.errors:
       return
-    save = self.cleaned_data.get('save')
-    name = self.cleaned_data.get('name')
+    save = cleaned_data.get('save')
+    name = cleaned_data.get('name')
     if save and not name:
       # Bother with name iff we're saving
       raise forms.ValidationError(_('Please enter a name'))
-    return self.cleaned_data
+    return cleaned_data
 
   def set_data(self, name, desc=''):
     """Set the name and desc programmatically"""

+ 48 - 11
apps/beeswax/src/beeswax/templates/execute.mako

@@ -247,20 +247,37 @@ ${layout.menubar(section='query')}
                 </form>
             </div>
         </div>
+
         <div id="querySide" class="span9">
             % if on_success_url:
               <input type="hidden" name="on_success_url" value="${on_success_url}"/>
             % endif
-
-            % if design and not design.is_auto and design.name:
-              <h1>${_('Query Editor')} : ${design.name}</h1>
-              % if design.desc:
-                <p>${design.desc}</p>
+            <div style="margin-bottom: 30px">
+              <h1>
+                ${ _('Query Editor') }
+                % if can_edit_name:
+                  :
+                  <a href="#" id="query-name" data-type="text" data-pk="${ design.id }"
+                     data-name="name"
+                     data-url="${ url(app_name + ':save_design_properties') }"
+                     data-original-title="${ _('Query name') }"
+                     data-value="${ design.name }"
+                     data-placement="left">
+                  </a>
+                %endif
+              </h1>
+              % if can_edit_name:
+                <p>
+                  <a href="#" id="query-description" data-type="textarea" data-pk="${ design.id }"
+                     data-name="description"
+                     data-url="${ url(app_name + ':save_design_properties') }"
+                     data-original-title="${ _('Query description') }"
+                     data-value="${ design.desc }"
+                     data-placement="bottom">
+                  </a>
+                </p>
               % endif
-            % else:
-              <h1>${_('Query Editor')}</h1>
-            % endif
-
+            </div>
             % if error_messages or log:
                 <ul class="nav nav-tabs">
                     <li class="active">
@@ -333,7 +350,7 @@ ${layout.menubar(section='query')}
         <div class="control-group">
             <label class="control-label">${_('Description')}</label>
             <div class="controls">
-            ${comps.field(form.saveform['desc'], tag='textarea', klass="input-xlarge")}
+              ${comps.field(form.saveform['desc'], klass="input-xlarge")}
             </div>
         </div>
       </form>
@@ -352,6 +369,9 @@ ${layout.menubar(section='query')}
 
 <link rel="stylesheet" href="/static/ext/css/codemirror-show-hint.css">
 
+<link href="/static/ext/css/bootstrap-editable.css" rel="stylesheet">
+<script src="/static/ext/js/bootstrap-editable.min.js"></script>
+
 <style>
   h1 {
     margin-bottom: 5px;
@@ -414,9 +434,26 @@ ${layout.menubar(section='query')}
 
 <script type="text/javascript" charset="utf-8">
     $(document).ready(function(){
-
       var queryPlaceholder = "${_('Example: SELECT * FROM tablename')}";
 
+      var successfunction = function(response, newValue) {
+        if (response.status != 0) {
+          $.jHueNotify.error("${ _('Problem: ') }" + response.data);
+        };
+      }
+
+      $("#query-name").editable({
+        'validate': function(value) {
+          if($.trim(value) == '') {
+            return "${ _('This field is required') }";
+          }
+        },
+        'success': successfunction
+      });
+      $("#query-description").editable({
+         'success': successfunction
+      });
+
       $("*[rel=tooltip]").tooltip({
         placement: 'bottom'
       });

+ 2 - 2
apps/beeswax/src/beeswax/templates/my_queries.mako

@@ -178,7 +178,7 @@ ${layout.menubar(section='my queries')}
       "bLengthChange":false,
       "bInfo":false,
       "aaSorting":[
-        [1, 'asc']
+        [3, "desc"]
       ],
       "aoColumns":[
         {"bSortable":false, "sWidth":"1%" },
@@ -198,7 +198,7 @@ ${layout.menubar(section='my queries')}
       "bLengthChange":false,
       "bInfo":false,
       "aaSorting":[
-        [1, 'desc']
+        [1, "desc"]
       ],
       "aoColumns":[
         {"bSortable":false},

+ 34 - 0
apps/beeswax/src/beeswax/tests.py

@@ -1411,6 +1411,40 @@ def test_beeswax_get_kerberos_security():
     finish()
 
 
+def test_save_design_properties():
+  client = make_logged_in_client()
+
+  resp = client.get('/beeswax/save_design_properties')
+  content = json.loads(resp.content)
+  assert_equal(-1, content['status'])
+
+  response = _make_query(client, 'SELECT', submission_type='Save', name='My Name', desc='My Description')
+  design = response.context['design']
+
+  try:
+    resp = client.post('/beeswax/save_design_properties', {'name': 'name', 'value': 'New Name', 'pk': design.id})
+    design = SavedQuery.objects.get(id=design.id)
+    content = json.loads(resp.content)
+    assert_equal(0, content['status'])
+    assert_equal('New Name', design.name)
+    assert_equal('My Description', design.desc)
+  finally:
+    design.delete()
+
+  response = _make_query(client, 'SELECT', submission_type='Save', name='My Name', desc='My Description')
+  design = response.context['design']
+
+  try:
+    resp = client.post('/beeswax/save_design_properties', {'name': 'description', 'value': 'New Description', 'pk': design.id})
+    design = SavedQuery.objects.get(id=design.id)
+    content = json.loads(resp.content)
+    assert_equal(0, content['status'])
+    assert_equal('My Name', design.name)
+    assert_equal('New Description', design.desc)
+  finally:
+    design.delete()
+
+
 def search_log_line(component, expected_log, all_logs):
   """Checks if 'expected_log' can be found in one line of 'all_logs' outputed by the logging component 'component'."""
   return re.compile('.+?%(component)s(.+?)%(expected_log)s' % {'component': component, 'expected_log': expected_log}).search(all_logs)

+ 1 - 0
apps/beeswax/src/beeswax/urls.py

@@ -28,6 +28,7 @@ urlpatterns = patterns('beeswax.views',
   url(r'^results/(?P<id>\d+)/(?P<first_row>\d+)$', 'view_results', name='view_results'),
   url(r'^download/(?P<id>\d+)/(?P<format>\w+)$', 'download', name='download'),
   url(r'^save_results/(?P<id>\d+)$', 'save_results', name='save_results'),
+  url(r'^save_design_properties$', 'save_design_properties', name='save_design_properties'), # Ajax
 
   url(r'^my_queries$', 'my_queries', name='my_queries'),
   url(r'^list_designs$', 'list_designs', name='list_designs'),

+ 24 - 1
apps/beeswax/src/beeswax/views.py

@@ -129,6 +129,28 @@ def save_design(request, form, type, design, explicit_save):
   return design
 
 
+def save_design_properties(request):
+  response = {'status': -1, 'data': ''}
+
+  try:
+    if request.method != 'POST':
+      raise PopupException(_('POST request required.'))
+
+    design_id = request.POST.get('pk')
+    design = authorized_get_design(request, design_id)
+
+    field = request.POST.get('name')
+    if field == 'name':
+      design.name = request.POST.get('value')
+    elif field == 'description':
+      design.desc = request.POST.get('value')
+    design.save()
+    response['status'] = 0
+  except Exception, e:
+    response['data'] = str(e)
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
 
 def delete_design(request):
   if request.method == 'POST':
@@ -368,14 +390,15 @@ def execute_query(request, design_id=None):
       # New design
       form.bind()
     form.query.fields['database'].choices = databases # Could not do it in the form
-
   return render('execute.mako', request, {
+
     'action': action,
     'design': design,
     'error_message': error_message,
     'form': form,
     'log': log,
     'on_success_url': on_success_url,
+    'can_edit_name': design and not design.is_auto and design.name,
   })
 
 

+ 480 - 0
desktop/core/static/ext/css/bootstrap-editable.css

@@ -0,0 +1,480 @@
+/*! X-editable - v1.4.3 
+* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
+* http://github.com/vitalets/x-editable
+* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
+
+.editableform {
+    margin-bottom: 0; /* overwrites bootstrap margin */
+}
+
+.editableform .control-group {
+    margin-bottom: 0; /* overwrites bootstrap margin */
+    white-space: nowrap; /* prevent wrapping buttons on new line */
+    line-height: 20px; /* overwriting bootstrap line-height. See #133 */
+}
+
+.editable-buttons {
+   display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
+   vertical-align: top;
+   margin-left: 7px;
+   /* inline-block emulation for IE7*/
+   zoom: 1; 
+   *display: inline;
+}
+
+
+
+.editable-input {
+    vertical-align: top; 
+    display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */
+    width: auto; /* bootstrap-responsive has width: 100% that breakes layout */
+    white-space: normal; /* reset white-space decalred in parent*/
+   /* display-inline emulation for IE7*/
+   zoom: 1; 
+   *display: inline;   
+}
+
+.editable-buttons .editable-cancel {
+   margin-left: 7px; 
+}
+
+/*for jquery-ui buttons need set height to look more pretty*/
+.editable-buttons button.ui-button-icon-only {
+   height: 24px; 
+   width: 30px;
+}
+
+.editableform-loading {
+    background: url('../img/loading.gif') center center no-repeat;  
+    height: 25px;
+    width: auto; 
+    min-width: 25px; 
+}
+
+.editable-inline .editableform-loading {
+    background-position: left 5px;      
+}
+
+ .editable-error-block {
+    max-width: 300px;
+    margin: 5px 0 0 0;
+    width: auto;
+    white-space: normal;
+}
+
+/*add padding for jquery ui*/
+.editable-error-block.ui-state-error {
+    padding: 3px;  
+}  
+
+.editable-error {
+   color: red;  
+}
+
+.editableform .editable-date {
+    padding: 0; 
+    margin: 0;
+    float: left;
+}
+
+
+/* checklist vertical alignment */
+.editable-checklist label input[type="checkbox"], 
+.editable-checklist label span {
+    vertical-align: middle;
+    margin: 0;
+}
+
+.editable-checklist label {
+    white-space: nowrap; 
+}
+
+/* set exact width of textarea to fit buttons toolbar */
+.editable-wysihtml5 {
+    width: 566px; 
+    height: 250px; 
+}
+
+/* clear button shown as link in date inputs */
+.editable-clear {
+   clear: both;
+   font-size: 0.9em;
+   text-decoration: none;
+   text-align: right;
+}
+
+/* IOS-style clear button for text inputs */
+.editable-clear-x {
+   background: url('../img/clear.png') center center no-repeat;
+   display: block;
+   width: 13px;    
+   height: 13px;
+   position: absolute;
+   opacity: 0.6;
+   z-index: 100;
+
+}
+
+.editable-clear-x:hover {
+   opacity: 1;
+}
+.editable-container {
+    max-width: none !important; /* without this rule poshytip/tooltip does not stretch */
+}  
+
+.editable-container.popover {
+/*   width: 300px;*/  /* debug */
+    width: auto; /* without this rule popover does not stretch */
+}
+
+.editable-container.editable-inline {
+    display: inline-block; 
+    vertical-align: middle;
+    width: auto;
+    /* inline-block emulation for IE7*/
+    zoom: 1; 
+    *display: inline;    
+}
+
+.editable-container.ui-widget {
+   font-size: inherit;  /* jqueryui widget font 1.1em too big, overwrite it */
+}
+.editable-click, 
+a.editable-click, 
+a.editable-click:hover {
+    text-decoration: none;
+    border-bottom: dashed 1px #0088cc;
+}
+
+.editable-click.editable-disabled, 
+a.editable-click.editable-disabled, 
+a.editable-click.editable-disabled:hover {
+   color: #585858;  
+   cursor: default;
+   border-bottom: none;
+}
+
+.editable-empty, .editable-empty:hover{
+  font-style: italic; 
+  color: #DD1144;  
+  border-bottom: none;
+  text-decoration: none;
+}
+
+.editable-unsaved {
+  font-weight: bold; 
+}
+
+.editable-unsaved:after {
+/*    content: '*'*/
+}
+
+/*see https://github.com/vitalets/x-editable/issues/139 */
+.form-horizontal .editable
+{ 
+    padding-top: 5px;
+    display:inline-block;
+}
+
+
+/*!
+ * Datepicker for Bootstrap
+ *
+ * Copyright 2012 Stefan Petre
+ * Improvements by Andrew Rowls
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+.datepicker {
+  padding: 4px;
+  margin-top: 1px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  direction: ltr;
+  /*.dow {
+		border-top: 1px solid #ddd !important;
+	}*/
+
+}
+.datepicker-inline {
+  width: 220px;
+}
+.datepicker.datepicker-rtl {
+  direction: rtl;
+}
+.datepicker.datepicker-rtl table tr td span {
+  float: right;
+}
+.datepicker-dropdown {
+  top: 0;
+  left: 0;
+}
+.datepicker-dropdown:before {
+  content: '';
+  display: inline-block;
+  border-left: 7px solid transparent;
+  border-right: 7px solid transparent;
+  border-bottom: 7px solid #ccc;
+  border-bottom-color: rgba(0, 0, 0, 0.2);
+  position: absolute;
+  top: -7px;
+  left: 6px;
+}
+.datepicker-dropdown:after {
+  content: '';
+  display: inline-block;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid #ffffff;
+  position: absolute;
+  top: -6px;
+  left: 7px;
+}
+.datepicker > div {
+  display: none;
+}
+.datepicker.days div.datepicker-days {
+  display: block;
+}
+.datepicker.months div.datepicker-months {
+  display: block;
+}
+.datepicker.years div.datepicker-years {
+  display: block;
+}
+.datepicker table {
+  margin: 0;
+}
+.datepicker td,
+.datepicker th {
+  text-align: center;
+  width: 20px;
+  height: 20px;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  border: none;
+}
+.table-striped .datepicker table tr td,
+.table-striped .datepicker table tr th {
+  background-color: transparent;
+}
+.datepicker table tr td.day:hover {
+  background: #eeeeee;
+  cursor: pointer;
+}
+.datepicker table tr td.old,
+.datepicker table tr td.new {
+  color: #999999;
+}
+.datepicker table tr td.disabled,
+.datepicker table tr td.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker table tr td.today,
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today.disabled:hover {
+  background-color: #fde19a;
+  background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a));
+  background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: -o-linear-gradient(top, #fdd49a, #fdf59a);
+  background-image: linear-gradient(top, #fdd49a, #fdf59a);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);
+  border-color: #fdf59a #fdf59a #fbed50;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+.datepicker table tr td.today:hover,
+.datepicker table tr td.today:hover:hover,
+.datepicker table tr td.today.disabled:hover,
+.datepicker table tr td.today.disabled:hover:hover,
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active,
+.datepicker table tr td.today.disabled,
+.datepicker table tr td.today:hover.disabled,
+.datepicker table tr td.today.disabled.disabled,
+.datepicker table tr td.today.disabled:hover.disabled,
+.datepicker table tr td.today[disabled],
+.datepicker table tr td.today:hover[disabled],
+.datepicker table tr td.today.disabled[disabled],
+.datepicker table tr td.today.disabled:hover[disabled] {
+  background-color: #fdf59a;
+}
+.datepicker table tr td.today:active,
+.datepicker table tr td.today:hover:active,
+.datepicker table tr td.today.disabled:active,
+.datepicker table tr td.today.disabled:hover:active,
+.datepicker table tr td.today.active,
+.datepicker table tr td.today:hover.active,
+.datepicker table tr td.today.disabled.active,
+.datepicker table tr td.today.disabled:hover.active {
+  background-color: #fbf069 \9;
+}
+.datepicker table tr td.active,
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td.active:hover,
+.datepicker table tr td.active:hover:hover,
+.datepicker table tr td.active.disabled:hover,
+.datepicker table tr td.active.disabled:hover:hover,
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active,
+.datepicker table tr td.active.disabled,
+.datepicker table tr td.active:hover.disabled,
+.datepicker table tr td.active.disabled.disabled,
+.datepicker table tr td.active.disabled:hover.disabled,
+.datepicker table tr td.active[disabled],
+.datepicker table tr td.active:hover[disabled],
+.datepicker table tr td.active.disabled[disabled],
+.datepicker table tr td.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker table tr td.active:active,
+.datepicker table tr td.active:hover:active,
+.datepicker table tr td.active.disabled:active,
+.datepicker table tr td.active.disabled:hover:active,
+.datepicker table tr td.active.active,
+.datepicker table tr td.active:hover.active,
+.datepicker table tr td.active.disabled.active,
+.datepicker table tr td.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker table tr td span {
+  display: block;
+  width: 23%;
+  height: 54px;
+  line-height: 54px;
+  float: left;
+  margin: 1%;
+  cursor: pointer;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.datepicker table tr td span:hover {
+  background: #eeeeee;
+}
+.datepicker table tr td span.disabled,
+.datepicker table tr td span.disabled:hover {
+  background: none;
+  color: #999999;
+  cursor: default;
+}
+.datepicker table tr td span.active,
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active.disabled:hover {
+  background-color: #006dcc;
+  background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -ms-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
+  background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
+  background-image: -o-linear-gradient(top, #0088cc, #0044cc);
+  background-image: linear-gradient(top, #0088cc, #0044cc);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);
+  border-color: #0044cc #0044cc #002a80;
+  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+  color: #fff;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.datepicker table tr td span.active:hover,
+.datepicker table tr td span.active:hover:hover,
+.datepicker table tr td span.active.disabled:hover,
+.datepicker table tr td span.active.disabled:hover:hover,
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active,
+.datepicker table tr td span.active.disabled,
+.datepicker table tr td span.active:hover.disabled,
+.datepicker table tr td span.active.disabled.disabled,
+.datepicker table tr td span.active.disabled:hover.disabled,
+.datepicker table tr td span.active[disabled],
+.datepicker table tr td span.active:hover[disabled],
+.datepicker table tr td span.active.disabled[disabled],
+.datepicker table tr td span.active.disabled:hover[disabled] {
+  background-color: #0044cc;
+}
+.datepicker table tr td span.active:active,
+.datepicker table tr td span.active:hover:active,
+.datepicker table tr td span.active.disabled:active,
+.datepicker table tr td span.active.disabled:hover:active,
+.datepicker table tr td span.active.active,
+.datepicker table tr td span.active:hover.active,
+.datepicker table tr td span.active.disabled.active,
+.datepicker table tr td span.active.disabled:hover.active {
+  background-color: #003399 \9;
+}
+.datepicker table tr td span.old {
+  color: #999999;
+}
+.datepicker th.switch {
+  width: 145px;
+}
+.datepicker thead tr:first-child th,
+.datepicker tfoot tr:first-child th {
+  cursor: pointer;
+}
+.datepicker thead tr:first-child th:hover,
+.datepicker tfoot tr:first-child th:hover {
+  background: #eeeeee;
+}
+.datepicker .cw {
+  font-size: 10px;
+  width: 12px;
+  padding: 0 2px 0 5px;
+  vertical-align: middle;
+}
+.datepicker thead tr:first-child th.cw {
+  cursor: default;
+  background-color: transparent;
+}
+.input-append.date .add-on i,
+.input-prepend.date .add-on i {
+  display: block;
+  cursor: pointer;
+  width: 16px;
+  height: 16px;
+}

文件差異過大導致無法顯示
+ 4 - 0
desktop/core/static/ext/js/bootstrap-editable.min.js


部分文件因文件數量過多而無法顯示