lanes 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/client/lanes/Config.coffee +3 -1
  4. data/client/lanes/access/screens/user-management/UserManagement.cjsx +2 -2
  5. data/client/lanes/components/grid/Body.cjsx +8 -1
  6. data/client/lanes/components/grid/EditingMixin.cjsx +5 -2
  7. data/client/lanes/components/grid/Grid.cjsx +1 -1
  8. data/client/lanes/components/grid/PopOverMixin.cjsx +1 -1
  9. data/client/lanes/components/grid/row-editor.scss +6 -0
  10. data/client/lanes/components/grid/styles.scss +1 -1
  11. data/client/lanes/components/record-finder/RecordFinder.cjsx +22 -16
  12. data/client/lanes/components/select-field/SelectField.cjsx +20 -27
  13. data/client/lanes/components/shared/DateTime.cjsx +12 -23
  14. data/client/lanes/components/shared/DisplayValue.cjsx +5 -10
  15. data/client/lanes/components/shared/FieldMixin.cjsx +107 -56
  16. data/client/lanes/components/shared/FieldWrapper.cjsx +59 -6
  17. data/client/lanes/components/shared/FormGroup.cjsx +3 -8
  18. data/client/lanes/components/shared/Helpers.coffee +1 -1
  19. data/client/lanes/components/shared/Icon.cjsx +1 -1
  20. data/client/lanes/components/shared/ImageAsset.cjsx +46 -0
  21. data/client/lanes/components/shared/Input.cjsx +5 -1
  22. data/client/lanes/components/shared/InputFieldMixin.cjsx +8 -27
  23. data/client/lanes/components/shared/NumberInput.cjsx +11 -4
  24. data/client/lanes/components/shared/RadioField.cjsx +18 -8
  25. data/client/lanes/components/shared/ScreenWrapper.cjsx +1 -1
  26. data/client/lanes/components/shared/TextArea.cjsx +15 -0
  27. data/client/lanes/components/shared/ToggleField.cjsx +11 -19
  28. data/client/lanes/components/shared/fields.scss +22 -76
  29. data/client/lanes/components/shared/{image-saver.scss → image-asset.scss} +1 -1
  30. data/client/lanes/components/shared/styles.scss +1 -1
  31. data/client/lanes/components/toolbar/RemoteChangeSets.cjsx +13 -10
  32. data/client/lanes/components/toolbar/changes-notification.scss +14 -1
  33. data/client/lanes/extension/Base.coffee +5 -1
  34. data/client/lanes/models/Asset.coffee +81 -0
  35. data/client/lanes/models/AssociationMap.coffee +14 -5
  36. data/client/lanes/models/AssociationProxy.coffee +9 -6
  37. data/client/lanes/models/Base.coffee +6 -3
  38. data/client/lanes/models/Collection.coffee +1 -3
  39. data/client/lanes/models/JobStatus.coffee +3 -0
  40. data/client/lanes/models/Query.coffee +3 -2
  41. data/client/lanes/models/Sync.coffee +6 -4
  42. data/client/lanes/react/mixins/Access.coffee +1 -1
  43. data/client/lanes/react/mixins/Data.coffee +33 -31
  44. data/client/lanes/screens/Commands.coffee +0 -1
  45. data/client/lanes/screens/Definitions.coffee +1 -1
  46. data/client/lanes/screens/SystemSettings.cjsx +23 -11
  47. data/client/lanes/screens/UserPreferences.cjsx +3 -2
  48. data/client/lanes/styles/bootstrap-custom-grid.scss +6 -4
  49. data/client/lanes/styles/fonts.scss +9 -0
  50. data/client/lanes/styles/global/styles.scss +1 -0
  51. data/client/lanes/styles/variables.scss +1 -1
  52. data/client/lanes/testing/Helpers.coffee +5 -3
  53. data/client/lanes/vendor/development/base.js +17206 -19211
  54. data/client/lanes/vendor/development/calendar.js +67 -471
  55. data/client/lanes/vendor/development/commons.js +21680 -19814
  56. data/client/lanes/vendor/development/helpers.js +40 -29
  57. data/client/lanes/vendor/development/toggle.js +22 -19
  58. data/client/lanes/vendor/development/widgets.js +2476 -2625
  59. data/client/lanes/vendor/production/base.js +19034 -21038
  60. data/client/lanes/vendor/production/calendar.js +67 -471
  61. data/client/lanes/vendor/production/commons.js +21369 -19136
  62. data/client/lanes/vendor/production/toggle.js +22 -19
  63. data/client/lanes/vendor/production/widgets.js +2476 -2625
  64. data/client/lanes/vendor/styles/widgets.scss +2 -0
  65. data/client/lanes/workspace/FirstRun.cjsx +69 -0
  66. data/client/lanes/workspace/Navbar.cjsx +4 -2
  67. data/client/lanes/workspace/ScreenView.cjsx +9 -1
  68. data/client/lanes/workspace/index.js +1 -0
  69. data/client/lanes/workspace/styles/screens.scss +11 -0
  70. data/config/database.yml +2 -2
  71. data/db/migrate/01_create_system_settings.rb +0 -1
  72. data/db/migrate/02_create_assets.rb +15 -0
  73. data/lanes.gemspec +25 -28
  74. data/lib/lanes/access/authentication_provider.rb +20 -7
  75. data/lib/lanes/access/role_collection.rb +9 -2
  76. data/lib/lanes/api/default_routes.rb +4 -4
  77. data/lib/lanes/api/formatted_reply.rb +15 -11
  78. data/lib/lanes/api/handlers/asset.rb +37 -0
  79. data/lib/lanes/api/request_wrapper.rb +18 -4
  80. data/lib/lanes/api/routing.rb +17 -6
  81. data/lib/lanes/api/updates.rb +1 -1
  82. data/lib/lanes/api.rb +1 -1
  83. data/lib/lanes/asset.rb +38 -0
  84. data/lib/lanes/concerns/all.rb +1 -1
  85. data/lib/lanes/concerns/asset_uploader.rb +60 -0
  86. data/lib/lanes/configuration.rb +1 -2
  87. data/lib/lanes/extension.rb +1 -1
  88. data/lib/lanes/logger.rb +26 -16
  89. data/lib/lanes/system_settings.rb +13 -8
  90. data/lib/lanes/version.rb +1 -1
  91. data/lib/lanes.rb +1 -0
  92. data/npm-build/base.js +1 -1
  93. data/npm-build/package.json +9 -9
  94. data/spec/command-reference-files/initial/Gemfile +1 -1
  95. data/spec/fixtures/logo.png +0 -0
  96. data/spec/lanes/components/grid/PopoverEditorSpec.coffee +48 -0
  97. data/spec/server/asset_spec.rb +34 -0
  98. data/spec/server/spec_helper.rb +14 -2
  99. metadata +118 -127
  100. data/client/lanes/components/shared/ControlLabel.cjsx +0 -45
  101. data/client/lanes/components/shared/ImageSaver.cjsx +0 -33
  102. data/client/lanes/models/SystemSettings.coffee +0 -0
  103. data/client/lanes/models/mixins/FileSupport.coffee +0 -60
  104. data/lib/lanes/api/handlers/file.rb +0 -26
  105. data/lib/lanes/concerns/image_uploader.rb +0 -42
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4672ec1ecf5544a9da08b8eb826b5e573987ac7d
4
- data.tar.gz: 53fb32ae3e09dec506b5cb9e1a961444c61bb122
3
+ metadata.gz: 98fcf2619dab60aa9576d6e71d67be4385a56ef6
4
+ data.tar.gz: a8d42b6cec864bc69e333bff826689378551c033
5
5
  SHA512:
6
- metadata.gz: a8a6599cce4d9aaa1e0bcea8969636d7d83ecbd6263e4d6a325743af0531287ce74dee0b6fb69577b6607906114fbe4483bf1516be174f3a767279fa53ac65e8
7
- data.tar.gz: f5e2136c4240d70c59c7ab485879e075d3600b0f4336e1b7da185823b4bdbcabaddae9abc38361f53b803c8fb4074583500b718df1f1e01db0eecaa6df1b975e
6
+ metadata.gz: 2e3d155815b2991ab8221bf0faca2e44005e44bcebe4900d9273ed7913d830a971b647faf9ba1002365de6113610a13a9286e876679c572fe5037fbe9f2ccd96
7
+ data.tar.gz: 7e51bfeecd2a829219816d435f7a636829d0e90f388ec85ff961cdbc9e5e2cbc8ecd4485ec3c0c04a240fdd5d84b7557e61130ed471b3487c6b002d6b25cb237
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ .byebug_history
1
2
  *.gem
2
3
  *.rbc
3
4
  .DS_Store
@@ -4,9 +4,11 @@ class SystemSettings extends Lanes.Models.Base
4
4
 
5
5
  props:
6
6
  id: {type:"integer"}
7
- logo: "file"
8
7
  settings: "object"
9
8
 
9
+ associations:
10
+ logo: { model: "Lanes.Models.Asset" }
11
+
10
12
  modelTypeIdentifier: -> 'system-settings'
11
13
  url: -> Lanes.config.api_path + '/system-settings'
12
14
  initialize: ->
@@ -19,7 +19,7 @@ class Lanes.Access.Screens.UserManagement extends Lanes.React.Screen
19
19
  id="role_names"
20
20
  key="row-select"
21
21
  queryModel={Lanes.Models.User}
22
- editOnly multiSelect writable unstyled
22
+ fieldOnly editOnly multiSelect writable
23
23
  model={model}
24
24
  labelField='name'
25
25
  getSelection={@rolesForUser}
@@ -33,7 +33,7 @@ class Lanes.Access.Screens.UserManagement extends Lanes.React.Screen
33
33
  <LC.ScreenWrapper identifier="user-management" flexVertical>
34
34
  <h1>Users Management</h1>
35
35
  <LC.Grid
36
- editorProps={height: 400, syncImmediatly: true}
36
+ editorProps={height: 350, syncImmediatly: true}
37
37
  query={@state.query}
38
38
  allowCreate
39
39
  editor={Lanes.Access.Screens.UserManagement.Editor}
@@ -15,7 +15,6 @@ class Lanes.Components.Grid.Body extends Lanes.React.BaseComponent
15
15
  )
16
16
  left = ev.clientX - ourBounds.left
17
17
  selectedIndex = (if @props.selectedIndex == index then null else index)
18
-
19
18
  selectedModel = if selectedIndex?
20
19
  @props.query.results.modelAt(selectedIndex, clone: true)
21
20
  else null
@@ -23,6 +22,14 @@ class Lanes.Components.Grid.Body extends Lanes.React.BaseComponent
23
22
  {top, left, container: ourBounds, rowHeight: clickBounds.height}
24
23
  )
25
24
 
25
+ getDefaultEditingPosition: ->
26
+ el = _.dom(@).el
27
+ container = el.getBoundingClientRect()
28
+ {
29
+ container, top: 0, left: container.width * 0.4
30
+ rowHeight: el.querySelector('.r')?.clientHeight || 50
31
+ }
32
+
26
33
  convertValue: (value, field) ->
27
34
  if @fieldConvertors[field.type] then @fieldConvertors[field.type](value) else value
28
35
 
@@ -16,6 +16,7 @@ Lanes.Components.Grid.EditingMixin = {
16
16
 
17
17
  editorTypes:
18
18
  text: (props) ->
19
+ props.value ||= ''
19
20
  <input type="text" {...props}
20
21
  onChange={_.partial(@onFieldChange, _, props.field)} />
21
22
  bigdec: (props) ->
@@ -33,7 +34,8 @@ Lanes.Components.Grid.EditingMixin = {
33
34
  listenNetworkEvents: true
34
35
  getDefaultProps: -> editors: {}
35
36
  componentDidMount: ->
36
- _.dom(@).qs('input').focusAndSelect()
37
+ qs = if @props.initiallyFocusedField then ".field[data-id=\"#{@props.initiallyFocusedField}\"] input" else 'input'
38
+ _.defer => _.dom(@).qs(qs).focusAndSelect()
37
39
 
38
40
  renderControls: ->
39
41
  if @props.allowDelete
@@ -55,7 +57,7 @@ Lanes.Components.Grid.EditingMixin = {
55
57
  </div>
56
58
 
57
59
  getFieldValue: (field) ->
58
- @props.model[field.id]
60
+ @props.model[field.id] || ''
59
61
 
60
62
  onDateFieldChange: (date, field) ->
61
63
  @props.model[field.id] = date
@@ -94,6 +96,7 @@ Lanes.Components.Grid.EditingMixin = {
94
96
  @renderDisplayValue(props)
95
97
 
96
98
  <div key={field.id}
99
+ data-id={field.id}
97
100
  style = {@props.cellStyles.styles[index]}
98
101
  className = {_.classnames('field', @props.cellStyles.classes[index])}
99
102
  >
@@ -62,7 +62,7 @@ class Lanes.Components.Grid extends Lanes.React.Component
62
62
  editing = _.extend({}, options, {
63
63
  index: index,
64
64
  model: options.model or @props.query.results.modelAt(index)
65
- position: options.position or (index * 50)
65
+ position: options.position || @refs.body.getDefaultEditingPosition()
66
66
  })
67
67
  set = (attrs = {}) =>
68
68
  if @props.editor and false isnt @props.commands?.isEditing()
@@ -22,7 +22,7 @@ Lanes.Components.Grid.PopoverMixin = {
22
22
  else
23
23
  props.placement = 'right'
24
24
  props.positionLeft = position.left
25
- props.arrowOffsetTop = Math.min(position.top, (@props.height - 75))
25
+ props.arrowOffsetTop = Math.min(position.top + 20, (@props.height - 75))
26
26
  props.positionTop = Math.max(5, position.top - props.arrowOffsetTop + (position.rowHeight / 2))
27
27
 
28
28
  <div className="editor po">
@@ -29,6 +29,12 @@
29
29
  &.center input { text-align: center; }
30
30
  &.right input { text-align: right; }
31
31
  }
32
+ .lanes-field, .form-group {
33
+ flex: 1;
34
+ display: flex;
35
+ align-items: center;
36
+ margin: 0;
37
+ }
32
38
  }
33
39
  .controls .buttons {
34
40
  background: $editor-background;
@@ -14,7 +14,7 @@
14
14
  .header, .r {
15
15
  display: flex;
16
16
  flex-direction: row;
17
-
17
+ min-height: 40px;
18
18
  display: flex;
19
19
  flex-direction: row;
20
20
  .c {
@@ -7,6 +7,10 @@ class Lanes.Components.RecordFinder extends Lanes.React.Component
7
7
  commands: React.PropTypes.object
8
8
  onModelSet: React.PropTypes.func
9
9
 
10
+ mixins: [
11
+ Lanes.Components.Form.InputFieldMixin
12
+ ]
13
+
10
14
  contextTypes:
11
15
  viewport: Lanes.PropTypes.State.isRequired
12
16
 
@@ -43,27 +47,29 @@ class Lanes.Components.RecordFinder extends Lanes.React.Component
43
47
  this.loadCurrentSelection()
44
48
  null
45
49
 
46
- getValue: ->
50
+ getValue: (ev) ->
47
51
  value = if @props.parentModel
48
52
  @props.parentModel[@props.associationName][@props.name]
49
53
  else
50
54
  @props.model[@props.name]
51
55
  value or ''
52
56
 
53
- render: ->
54
- findIcon = <button className="btn btn-primary icon icon-search icon-lg" onClick={@showFinder}/>
57
+
58
+ renderInputField: (props, handlers) ->
59
+ # editOnly writable
55
60
  model = @props.parentModel or @props.model
56
- label =
57
- <LC.ControlLabel titleOnly {...@props} model={model} />
58
61
 
59
- <LC.Input
60
- ref="input"
61
- groupClassName="record-finder"
62
- editOnly writable
63
- name={@props.query.initialField.id}
64
- onKeyPress={@onKeyPress}
65
- {...@props}
66
- label={label}
67
- model={@modelForAccess()}
68
- getValue={@getValue}
69
- buttonAfter={findIcon} />
62
+ <BS.InputGroup>
63
+
64
+ <BS.FormControl
65
+ {...props} {...handlers}
66
+ onChange={@fieldMixinSetValue}
67
+ value={@getValue()}
68
+ onKeyPress={@onKeyPress}
69
+ />
70
+
71
+ <BS.InputGroup.Button>
72
+ <button className="btn btn-primary icon icon-search icon-lg"
73
+ onClick={@showFinder} />
74
+ </BS.InputGroup.Button>
75
+ </BS.InputGroup>
@@ -18,6 +18,8 @@ class Lanes.Components.SelectField extends Lanes.React.Component
18
18
  getDefaultProps: ->
19
19
  labelField: 'label', idField: 'id'
20
20
 
21
+ fieldClassName: 'select'
22
+
21
23
  dataObjects:
22
24
  query: ->
23
25
  src = @props.queryModel or
@@ -31,16 +33,6 @@ class Lanes.Components.SelectField extends Lanes.React.Component
31
33
  ]
32
34
  })
33
35
 
34
- renderDisplayValue: ->
35
- value = @getValue()
36
- label = if _.isArray(value)
37
- _.toSentence( _.map(value, @props.labelField) )
38
- else if _.isObject(value)
39
- value[@props.labelField]
40
- else
41
- value
42
- <span>{label}</span>
43
-
44
36
  getValue: ->
45
37
  return undefined if @state.isOpen and not @props.multiSelect
46
38
  return @state.tempDisplayValue if @state.tempDisplayValue
@@ -102,10 +94,23 @@ class Lanes.Components.SelectField extends Lanes.React.Component
102
94
  isBusy: ->
103
95
  !!(@state.requestInProgress or @query.results.requestInProgress)
104
96
 
105
- renderEdit: (label) ->
106
- props = _.omit(@props, 'label')
107
- Component = Lanes.Vendor.ReactWidgets[if @props.multiSelect then 'Multiselect' else 'Combobox']
108
- select = <Component
97
+ renderDisplay: (props) ->
98
+ value = @getValue()
99
+ label = if _.isArray(value)
100
+ _.toSentence( _.map(value, @props.labelField) )
101
+ else if _.isObject(value)
102
+ value[@props.labelField]
103
+ else
104
+ value
105
+ <BS.FormControl.Static {...props}>
106
+ {label}
107
+ </BS.FormControl.Static>
108
+
109
+
110
+ renderEdit: (props) ->
111
+ type = if @props.multiSelect then 'Multiselect' else 'Combobox'
112
+ Component = Lanes.Vendor.ReactWidgets[type]
113
+ <Component
109
114
  ref="select"
110
115
  className={@props.className}
111
116
  open={@state.isOpen}
@@ -121,16 +126,4 @@ class Lanes.Components.SelectField extends Lanes.React.Component
121
126
  onBlur={@onFieldInteraction}
122
127
  value={@getValue()}
123
128
  {...props}
124
- />
125
-
126
-
127
- if @props.unstyled
128
- select
129
- else
130
- <LC.FormGroup
131
- {...@props}
132
- className={@formGroupClassNames()}
133
- label={@getLabelValue()}
134
- >
135
- {select}
136
- </LC.FormGroup>
129
+ />
@@ -3,7 +3,7 @@ class Lanes.Components.DateTime extends Lanes.React.Component
3
3
  mixins: [
4
4
  Lanes.Components.Form.FieldMixin
5
5
  ]
6
- formGroupClass: 'date-time'
6
+ fieldClassName: 'date-time'
7
7
 
8
8
  getDefaultProps: ->
9
9
  format: 'ddd, MMM Do YYYY, h:mm a'
@@ -12,37 +12,26 @@ class Lanes.Components.DateTime extends Lanes.React.Component
12
12
  unlabled: React.PropTypes.bool
13
13
  format: React.PropTypes.string
14
14
 
15
- renderDisplayValue: ->
16
- <span>{_.moment(@model[@props.name]).format(@props.format)}</span>
17
-
18
- getValue: ->
19
- @refs.input.getValue()
20
15
 
21
16
  handleKeyDown: (ev) ->
22
17
  @props.onEnter() if ev.key is 'Enter'
23
18
 
24
19
  handleDateTimeChange: (val) ->
25
- @handleChange({target: {value: val}})
20
+ @fieldMixinSetValue({target: {value: val}})
21
+
26
22
 
27
- renderEdit: (label) ->
28
- value = @_getValue() or ''
23
+ renderDisplay: (props) ->
24
+ <BS.FormControl.Static {...props}>
25
+ {_.moment(@model[@props.name]).format(@props.format)}
26
+ </BS.FormControl.Static>
27
+
28
+ renderEdit: (props) ->
29
29
  props = _.extend({
30
30
  ref: 'control'
31
- className: _.classnames('value', changeset: @state.changeset)
32
- label: if @props.unlabeled then false else label
33
- value: value
31
+ value: @fieldMixinGetValue()
34
32
  onChange: @handleDateTimeChange
35
33
  }, @props)
36
- if @props.inputOnly then @renderPlain(props) else @renderStyled(props, label)
37
-
38
- renderPlain: (props) ->
39
- <Lanes.Vendor.ReactWidgets.DateTimePicker {...props} />
40
34
 
41
- renderStyled: (props, label) ->
42
- <LC.FormGroup
35
+ <Lanes.Vendor.ReactWidgets.DateTimePicker
43
36
  {...props}
44
- className={@formGroupClassNames()}
45
- label={label}
46
- >
47
- <Lanes.Vendor.ReactWidgets.DateTimePicker {...props} />
48
- </LC.FormGroup>
37
+ />
@@ -3,14 +3,9 @@ class Lanes.Components.DisplayValue extends Lanes.React.Component
3
3
  mixins: [
4
4
  Lanes.Components.Form.FieldMixin
5
5
  ]
6
- formGroupClass: 'display'
6
+ fieldClassName: 'display-value'
7
7
 
8
- renderEdit: (label) ->
9
- value = @_getValue()
10
- <LC.FormGroup
11
- {...@props}
12
- className={@formGroupClassNames()}
13
- label={@getLabelValue()}
14
- >
15
- {value}
16
- </LC.FormGroup>
8
+ renderEdit: (props) ->
9
+ <BS.FormControl.Static {...props}>
10
+ {@fieldMixinGetValue()}
11
+ </BS.FormControl.Static>
@@ -1,38 +1,40 @@
1
1
  Lanes.Components.Form || = {}
2
2
 
3
+
3
4
  Lanes.Components.Form.FieldMixin = {
5
+
4
6
  bindDataEvents: ->
5
7
  model: "change:#{@props.name} remote-update:#{@props.name} invalid-fields invalid-field:#{@getInvalidFieldName()}"
6
8
 
7
9
  mixins: [
8
10
  Lanes.React.Mixins.Access
9
- Lanes.React.Mixins.FieldErrors
10
11
  Lanes.React.Mixins.ReadEditingState
12
+
13
+ Lanes.React.Mixins.FieldErrors
11
14
  ]
12
15
 
13
16
  pubsub: false
14
17
  propTypes:
15
- model: Lanes.PropTypes.State.isRequired
16
- name: React.PropTypes.string.isRequired
18
+ model: Lanes.PropTypes.State.isRequired
19
+ name: React.PropTypes.string.isRequired
20
+ unlabeled: React.PropTypes.bool
21
+ fieldOnly: React.PropTypes.bool
22
+ onChange: React.PropTypes.func
23
+ unstyled: React.PropTypes.bool
24
+ getValue: React.PropTypes.func
25
+ setValue: React.PropTypes.func
17
26
  align: React.PropTypes.oneOf([
18
27
  'right', 'left', 'center'
19
28
  ])
20
29
  label: React.PropTypes.oneOfType([
21
30
  React.PropTypes.string, React.PropTypes.element
22
31
  ])
23
- onChange: React.PropTypes.func
24
- unstyled: React.PropTypes.bool
25
- getValue: React.PropTypes.func
26
- setValue: React.PropTypes.func
27
-
28
- _getValue: ->
29
- if @props.getValue
30
- @props.getValue.call(@model, @props) or ''
31
- else
32
- @model[@props.name] or ''
33
32
 
34
- _setValue: (value) ->
35
- if @props.setValue then @props.setValue(value) else @model[@props.name] = value
33
+ fieldMixinSetValue: (ev) ->
34
+ if @props.onChange
35
+ @props.onChange(ev)
36
+ unless true is ev.isDefaultPrevented?()
37
+ @model[@props.name] = ev.target.value
36
38
 
37
39
  componentWillUnmount: ->
38
40
  clearTimeout(@state.pendingChangeSetDelay) if @state.pendingChangeSetDelay
@@ -42,61 +44,110 @@ Lanes.Components.Form.FieldMixin = {
42
44
 
43
45
  setDataState: (state, evname) ->
44
46
  displayChangeset = @model.updatingFromChangeset and @model.changedAttributes()[@props.name]
45
- if displayChangeset
47
+ if displayChangeset and not @state.pendingChangeSetDelay
46
48
  pendingChangeSetDelay = _.delay(@_unsetChangeSet, 2000)
47
49
  @setState(_.extend( state, {pendingChangeSetDelay, displayChangeset}))
48
50
 
49
- handleChange: (ev) ->
50
- if @props.onChange
51
- @props.onChange(ev)
51
+ _fieldMixinGetLabelValue: ->
52
+ @getLabelValue?() ||
53
+ @props.label ||
54
+ _.titleize _.humanize @props.name
55
+
56
+ fieldMixinGetValue: ->
57
+ value = if @props.getValue
58
+ @props.getValue.call(@model, @props)
59
+ else if @props.value?
60
+ @props.value
61
+ else if @getValue
62
+ @getValue()
52
63
  else
53
- @_setValue(ev.target.value)
54
- null
55
-
56
- renderMixinDisplayValue: ->
57
- value = @_getValue() || ""
58
- value = String(value) if _.isObject(value) or not value
59
- <span>{value}</span>
60
-
61
- formGroupClassNames: ->
62
- _.classnames( _.result(this, 'formGroupClass'), {
63
- changeset: @state.displayChangeset
64
- })
65
-
66
- _mixinRenderValue: (label, className) ->
67
- value = (@renderDisplayValue || @renderMixinDisplayValue)?()
68
- className = _.classnames(@props.className, className, @formGroupClassNames(), {
69
- "align-#{@props.align}": @props.align
70
- })
71
- if @props.unstyled
72
- value
64
+ @model[@props.name]
65
+ if _.isBlank(value)
66
+ ''
73
67
  else
74
- <LC.FormGroup display {...@props}
75
- className={className} label={label}
76
- >
77
- {value}
78
- </LC.FormGroup>
79
-
80
- getLabelValue: ->
81
- @props.label || _.titleize _.humanize @props.name
68
+ value
82
69
 
83
- render: ->
70
+ _fieldMixinRenderFormGroup: (child, props, options) ->
71
+ if (invalidMsg = @fieldInvalidValueMessage())
72
+ msg = <BS.HelpBlock>{invalidMsg}</BS.HelpBlock>
84
73
  unless @props.unlabeled
85
74
  label =
86
- <LC.ControlLabel {...@props} titleOnly
87
- label={@getLabelValue()} />
88
-
75
+ <BS.ControlLabel>
76
+ {@_fieldMixinGetLabelValue()}
77
+ </BS.ControlLabel>
78
+
79
+ <BS.Col {...props}>
80
+ <BS.FormGroup validationState={options.validationState}>
81
+ {label}
82
+ {child}
83
+ <BS.FormControl.Feedback />
84
+ {msg}
85
+ </BS.FormGroup>
86
+ </BS.Col>
87
+
88
+ _fieldMixinRenderEdit: (props) ->
89
+ <BS.FormControl
90
+ type={@props.type || "text"}
91
+ value={@fieldMixinGetValue()}
92
+ {...props}
93
+ />
94
+
95
+ _fieldMixinRenderDisplay: (props) ->
96
+ value = @fieldMixinGetValue()
97
+ value = value.toString() if _.isObject(value)
98
+
99
+ <BS.FormControl.Static {...props}>
100
+ {value}
101
+ </BS.FormControl.Static>
102
+
103
+ _fieldMixinRenderNone: (props) ->
104
+ <span {...props} />
105
+
106
+ renderType: ->
89
107
  if @isEditingRecord()
90
108
  if @hasWriteAccess()
91
- @renderEdit(label)
109
+ ['edit', 'Edit']
92
110
  else if @hasReadAccess()
93
- (@renderReadOnly || @_mixinRenderValue)(label, "read-only")
111
+ ['read-only', 'Display']
94
112
  else
95
- <span />
113
+ ['none', 'None']
96
114
  else
97
115
  if @hasReadAccess()
98
- (@renderDisplay || @_mixinRenderValue)(label, "display")
116
+ ['display', 'Display']
99
117
  else
100
- <span/>
118
+ ['none', 'None']
119
+ statics:
120
+ cleanColumnProps: (props) ->
121
+ _.omit props, 'model', 'label', 'name', 'unlabeled', 'fieldOnly', 'placeholder', 'type'
122
+
123
+ renderEmptyColumn: (props = @props) ->
124
+ props = @cleanColumnProps(props)
125
+ <BS.Col {...props} />
126
+
127
+ render: ->
128
+ [type, method] = @renderType()
129
+ options = {}
130
+
131
+ hasError = @isFieldValueInvalid()
132
+ options.validationState = 'warning' if hasError
133
+ props = LC.Form.FieldMixin.statics.cleanColumnProps(@props)
134
+
135
+ props.className = _.classnames(
136
+ _.result(this, 'fieldClassName'),
137
+ 'lanes-field', type, props.className,
138
+ ( if @props.align then "align-#{@props.align}" else null),
139
+ {
140
+ changeset: @state.displayChangeset
141
+ 'has-error': hasError
142
+ }
143
+ )
144
+
145
+ field = (@[ "render#{method}" ] || @["_fieldMixinRender#{method}"])(
146
+ if @props.fieldOnly then props else _.omit(props, 'className')
147
+ )
148
+ if @props.fieldOnly
149
+ field
150
+ else
151
+ ( @['renderFormGroup'] || @['_fieldMixinRenderFormGroup'] )(field, props, options)
101
152
 
102
153
  }
@@ -1,13 +1,66 @@
1
1
  class Lanes.Components.FieldWrapper extends Lanes.React.BaseComponent
2
2
 
3
3
  mixins: [
4
+ Lanes.React.Mixins.Access
5
+ Lanes.React.Mixins.FieldErrors
4
6
  Lanes.React.Mixins.ReadEditingState
5
7
  ]
8
+ blankElement: 'span'
9
+ propTypes:
10
+ model: Lanes.PropTypes.State.isRequired
11
+ unlabeled: React.PropTypes.bool
12
+ displayComponent: React.PropTypes.any.isRequired
13
+ label: React.PropTypes.oneOfType([
14
+ React.PropTypes.string, React.PropTypes.element
15
+ ])
16
+
17
+ renderLabel: ->
18
+ return null if @props.unlabeled
19
+ <BS.ControlLabel>
20
+ {@props.label}
21
+ </BS.ControlLabel>
22
+
23
+
24
+ renderType: ->
25
+ if @isEditingRecord()
26
+ if @hasWriteAccess()
27
+ ['edit', @props.children]
28
+ else if @hasReadAccess()
29
+ ['display']
30
+ else
31
+ ['none', @blankElement]
32
+ else
33
+ if @hasReadAccess()
34
+ ['display']
35
+ else
36
+ ['none', @blankElement]
6
37
 
7
38
  render: ->
8
- props = _.omit(@props, 'value')
9
- isEdit = @isEditingRecord()
10
- className = _.classnames(props.className, display: !isEdit)
11
- <LC.FormGroup {...props} className={className}>
12
- {if isEdit then @props.children else @props.value}
13
- </LC.FormGroup>
39
+ [type, child] = @renderType()
40
+ props = _.omit(@props,
41
+ 'value', 'model', 'value', 'label', 'name'
42
+ )
43
+ unless child
44
+ Comp = @props.displayComponent
45
+ child = <Comp props />
46
+
47
+ if @isFieldValueInvalid()
48
+ validationState = 'warning'
49
+
50
+ if (invalidMsg = @fieldInvalidValueMessage())
51
+ msg = <BS.HelpBlock>{invalidMsg}</BS.HelpBlock>
52
+
53
+ className = _.classnames( 'lanes-field', type, @props.className
54
+ ( if @props.align then "align-#{@props.align}" else null),
55
+ )
56
+
57
+ <BS.Col {...props} className={className}>
58
+ <BS.FormGroup validationState={validationState}>
59
+ <BS.ControlLabel>
60
+ {@renderLabel()}
61
+ </BS.ControlLabel>
62
+ {child}
63
+ <BS.FormControl.Feedback />
64
+ {msg}
65
+ </BS.FormGroup>
66
+ </BS.Col>
@@ -20,18 +20,13 @@ class Lanes.Components.FormGroup extends Lanes.React.Component
20
20
  display: false == @props.editing
21
21
  'has-error': @isFieldValueInvalid()
22
22
  )
23
-
24
23
  colProps = _.omit(@props, 'name', 'label', 'type', 'editing', 'display')
25
24
  valueClassNames = _.classnames('value', {
26
25
  "align-#{@props.align}": @props.align
27
26
  })
28
27
  <BS.Col {...colProps} className={className}>
29
- <div className="form-group">
28
+ <BS.FormGroup className={valueClassNames}>
30
29
  <LC.ControlLabel {...@props} />
31
- <div className="input-group" name={@props.name}>
32
- <div className={valueClassNames}>
33
- {@props.children}
34
- </div>
35
- </div>
36
- </div>
30
+ {@props.children}
31
+ </BS.FormGroup>
37
32
  </BS.Col>