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
@@ -3,7 +3,7 @@ Lanes.Components.Helpers = {
3
3
  modelLinkFields: (model) ->
4
4
  return (field) ->
5
5
  onChange: (ev) -> model[field] = ev.target.value
6
- value: _.result(model, field)
6
+ value: _.result(model, field) or ''
7
7
 
8
8
 
9
9
  }
@@ -17,4 +17,4 @@ class Lanes.Components.Icon extends Lanes.React.Component
17
17
  'icon-4x' : @props['4x']
18
18
  'icon-5x' : @props['5x']
19
19
 
20
- <i style={@props.style} className={classes} />
20
+ <i {...@props} style={@props.style} className={classes} />
@@ -0,0 +1,46 @@
1
+ class Lanes.Components.ImageAsset extends Lanes.React.Component
2
+
3
+ propTypes:
4
+ model: Lanes.PropTypes.Model.isRequired
5
+ name: React.PropTypes.string.isRequired
6
+ size: React.PropTypes.oneOf([
7
+ 'thumb', 'medium', 'original'
8
+ ]).isRequired
9
+
10
+ dataObjects:
11
+ asset: -> @props.model[@props.name]
12
+
13
+ listenNetworkEvents: true
14
+
15
+ bindDataEvents: ->
16
+ model: "change:#{@props.name} change:#{@props.name}_data"
17
+
18
+ handleImageChange: (ev) ->
19
+ ev.preventDefault()
20
+ @asset.blob = ev.target.files[0]
21
+
22
+ renderImage: ->
23
+ <img className="preview" src={@asset.thumbnail_url} />
24
+
25
+ blankImage: ->
26
+ null
27
+
28
+ render: ->
29
+ Component = if @asset.hasImage then @renderImage else @blankImage
30
+ className = _.classnames('image-asset', @props.className, {
31
+ 'with-image': @asset.hasImage
32
+ })
33
+
34
+ <LC.FieldWrapper
35
+ {...@props}
36
+ className={className}
37
+ displayComponent={Component}
38
+ >
39
+ <Component />
40
+ <form>
41
+ <label className="selector">
42
+ <span>{if @asset.hasImage then 'Update' else 'Choose'}</span>
43
+ <input id='file' className="file" type="file" onChange={@handleImageChange} />
44
+ </label>
45
+ </form>
46
+ </LC.FieldWrapper>
@@ -5,4 +5,8 @@ class Lanes.Components.Input extends Lanes.React.Component
5
5
  ]
6
6
 
7
7
  renderInputField: (props, handlers) ->
8
- <BS.Input standalone {...props} {...handlers} />
8
+ <BS.FormControl
9
+ {...props}
10
+ {...handlers}
11
+ onChange={@fieldMixinSetValue}
12
+ />
@@ -5,16 +5,12 @@ Lanes.Components.Form.InputFieldMixin =
5
5
  ]
6
6
 
7
7
  propTypes:
8
- unlabled: React.PropTypes.bool
9
8
  onlyNumeric: React.PropTypes.bool
10
9
  selctOnFocus: React.PropTypes.bool
11
10
 
12
11
  getDefaultProps: ->
13
12
  type: 'text'
14
13
 
15
- getValue: ->
16
- @refs.input.getValue() or ''
17
-
18
14
  handleKeyDown: (ev) ->
19
15
  @props.onEnter() if ev.key is 'Enter'
20
16
 
@@ -25,31 +21,16 @@ Lanes.Components.Form.InputFieldMixin =
25
21
  @onFieldInteraction()
26
22
  @props.onBlur?()
27
23
 
28
- renderEdit: (label) ->
29
- value = @props.value or @_getValue() or ''
30
-
31
- props = _.extend({
32
- ref: 'input'
33
- className: _.classnames('edit',
34
- changeset: @state.changeset
35
- )
36
- label: if @props.unlabeled then false else label
37
- }, @props, {value: value})
24
+ renderEdit: (props) ->
25
+ props = _.extend(props, {
26
+ ref: 'input'
27
+ name: @props.name
28
+ value: @fieldMixinGetValue()
29
+ })
38
30
 
39
- handlers = { onBlur: @onFieldBlur, onChange: @handleChange }
31
+ handlers = { onBlur: @onFieldBlur }
40
32
 
41
- if @isFieldValueInvalid() then props.bsStyle = 'error'
42
33
  if @props.onEnter then handlers.onKeyDown = @handleKeyDown
43
34
  if @props.selectOnFocus then handlers.onFocus = @selectOnFocus
44
35
 
45
- props = _.omit(props, 'label')
46
- field = @renderInputField(props, handlers)
47
-
48
- if props.inputOnly
49
- field
50
- else
51
- label ||= @props.label or _.field2title(@props.name)
52
-
53
- <LC.FormGroup display {...props} label={label}>
54
- {field}
55
- </LC.FormGroup>
36
+ @renderInputField(props, handlers)
@@ -1,19 +1,26 @@
1
+ class FakeNumberEvent
2
+ constructor: (value) ->
3
+ value = if _.isNull(value) then 0 else value
4
+ @target = {value}
5
+ isDefaultPrevented: -> false
6
+
1
7
  class Lanes.Components.NumberInput extends Lanes.React.Component
2
8
 
3
9
  mixins: [
4
10
  Lanes.Components.Form.InputFieldMixin
5
11
  ]
12
+
6
13
  handleNumberChange: (n) ->
7
- value = if _.isNull(n) then 0 else n
8
- @handleChange(target: {value})
14
+ @fieldMixinSetValue( new FakeNumberEvent(n) )
15
+
16
+ renderInputField: (props, handlers) ->
9
17
 
10
- renderInputField: (props, handlers, label) ->
11
18
  props.format ||= '#,###.00'
12
19
  props = _.omit(props, 'label')
13
20
 
14
21
  <Lanes.Vendor.ReactWidgets.NumberPicker
15
22
  ref="select"
16
- className={@props.className}
23
+
17
24
  {...handlers}
18
25
  {...props}
19
26
  onChange={@handleNumberChange}
@@ -1,14 +1,24 @@
1
+ class FakeInputEvent
2
+ constructor: (value) ->
3
+ @target = {value}
4
+ isDefaultPrevented: -> false
5
+
6
+
1
7
  class Lanes.Components.RadioField extends Lanes.React.Component
2
8
 
3
9
  mixins: [
4
10
  Lanes.Components.Form.FieldMixin
5
11
  ]
6
12
 
7
- renderEdit: ->
8
- <BS.Col {...@props}>
9
- <BS.Input
10
- type="radio"
11
- checked={@props.checked? || @props.value == @model[@props.name]}
12
- onChange={@handleChange}
13
- {...@props} />
14
- </BS.Col>
13
+ handleRadioChange: (ev) ->
14
+ if ev.target.checked
15
+ @fieldMixinSetValue( new FakeInputEvent(@props.value) )
16
+
17
+ renderEdit: (props, handlers) ->
18
+ <BS.FormControl
19
+ {...props}
20
+ {...handlers}
21
+ type="radio"
22
+ checked={@props.checked? || @props.value == @model[@props.name]}
23
+ onChange={@handleRadioChange}
24
+ />
@@ -8,6 +8,6 @@ class Lanes.Components.ScreenWrapper extends Lanes.React.Component
8
8
  'screen-wrapper', @props.identifier,
9
9
  'flex-vertically': @props.flexVertical
10
10
  )
11
- <div className={classes}>
11
+ <div className={classes} style={@props.style}>
12
12
  {@props.children}
13
13
  </div>
@@ -0,0 +1,15 @@
1
+ class Lanes.Components.TextArea extends Lanes.React.Component
2
+
3
+ mixins: [
4
+ Lanes.Components.Form.InputFieldMixin
5
+ ]
6
+
7
+ renderInputField: (props, handlers) ->
8
+ className = _.classnames(props.className, 'form-control')
9
+ <textarea
10
+ {...props}
11
+ {...handlers}
12
+ {..._.pick(@props, 'placeholder')}
13
+ className={className}
14
+ onChange={@fieldMixinSetValue}
15
+ />
@@ -1,31 +1,23 @@
1
1
  class Lanes.Components.ToggleField extends Lanes.React.Component
2
2
  mixins: [ Lanes.Components.Form.FieldMixin ]
3
3
 
4
- formGroupClass: 'toggle'
4
+ fieldClassName: 'toggle'
5
5
 
6
- renderDisplayValue: ->
6
+ handleToggleChange: (ev) ->
7
+ @props.model[@props.name] = ev.target.checked
8
+ null
9
+
10
+ renderDisplay: (props) ->
7
11
  <Lanes.Vendor.ReactToggle
12
+ {...props}
8
13
  checked={!!@props.model[@props.name]}
9
14
  disabled={true}
10
15
  />
11
16
 
12
- handleToggleChange: (ev) ->
13
- @props.model[@props.name] = ev.target.checked
14
- null
17
+ renderEdit: (props) ->
15
18
 
16
- renderEdit: (label) ->
17
- props = _.omit(@props, 'label')
18
- toggle = <Lanes.Vendor.ReactToggle
19
+ <Lanes.Vendor.ReactToggle
20
+ {...props}
19
21
  onChange={@handleToggleChange}
20
22
  checked={!!@props.model[@props.name]}
21
- />
22
- if @props.unstyled
23
- toggle
24
- else
25
- <LC.FormGroup
26
- {...props}
27
- className={@formGroupClassNames()}
28
- label={label}
29
- >
30
- {toggle}
31
- </LC.FormGroup>
23
+ />
@@ -3,51 +3,37 @@
3
3
 
4
4
  $lanes-field-margin: 6px;
5
5
 
6
- label.field {
7
- display: block;
8
- }
9
- .field {
10
- font-weight: normal;
11
- .input-group {
12
- font-weight: normal;
13
- width: 100%;
14
- }
6
+
7
+
8
+ .lanes-field {
9
+
10
+ .control-label {display: block;}
11
+
15
12
  &.display {
16
- .input-group {
17
- display: block;
18
- }
19
- .value {
20
- padding-left: $lanes-field-margin;
21
- padding-right: $lanes-field-margin;
22
- display: block;
23
- height: $lanes-field-value-height;
24
- line-height: $lanes-field-value-height;
13
+ .form-group {
25
14
  border-bottom: 1px solid $input-border;
15
+ min-height: 57px; // 55 + 1px border
16
+ }
17
+ &.toggle {
18
+ .form-group { border-bottom: 0; }
19
+ .react-toggle { cursor: inherit; }
26
20
  }
27
21
 
28
22
  }
29
- &.edit {
30
- // bootstrap styles
31
- .value > div > input:only-child {
32
- border-top-left-radius: $border-radius-base;
33
- border-top-right-radius: $border-radius-base;
34
- border-bottom-left-radius: $border-radius-base;
35
- border-bottom-right-radius: $border-radius-base;
36
- }
23
+
24
+ &.align-right{
25
+ text-align: right;
26
+ .control-label, .value, .rw-widget input {text-align: right; }
37
27
  }
38
- &.read-only .input-group {
39
- @extend .form-control !optional;
40
- padding: 6px 12px;
41
- cursor: not-allowed;
28
+ &.align-center{
29
+ text-align: center;
30
+ .control-label, .value, .rw-widget input { text-align: center; }
42
31
  }
43
- .label {
44
- padding: 0;
45
- font-size: 85%;
32
+
33
+ &.select {
34
+ .form-control-feedback { right: 30px; }
46
35
  }
47
- &.read-only .title { margin-bottom: 5px; }
48
- }
49
36
 
50
- .field, {
51
37
  .form-group {
52
38
  background-color: rgba(255, 255, 255, 1);
53
39
  transition: background-color 1000ms linear;
@@ -63,44 +49,4 @@ label.field {
63
49
  background-color: darken($color-rgba, 20%);
64
50
  }
65
51
  }
66
- &.display {
67
- &.toggle {
68
- .value { border-bottom: 0; }
69
- .react-toggle { cursor: inherit; }
70
- }
71
- &.editing .react-toggle { margin-top: 5px; }
72
- .value span {
73
- display: inline-block;
74
- text-overflow: ellipsis;
75
- white-space: nowrap;
76
- overflow: hidden;
77
- width: 100%;
78
- }
79
- }
80
- .label-title {
81
- display: inline-block;
82
- }
83
- label {
84
- display: block;
85
- overflow: hidden;
86
- text-overflow: ellipsis;
87
- white-space: nowrap;
88
- width: 100%;
89
- }
90
- i.error {
91
- margin-left: $lanes-field-margin;
92
- }
93
- .control-label {
94
- margin-left: $lanes-field-margin;
95
- margin-right: $lanes-field-margin;
96
- }
97
-
98
-
99
- }
100
-
101
- .align-right{
102
- .control-label, .value, .rw-widget input {text-align: right; }
103
- }
104
- .align-center{
105
- .control-label, .value, .rw-widget input { text-align: center; }
106
52
  }
@@ -1,6 +1,6 @@
1
1
  @import "../../styles/bootstrap/buttons";
2
2
 
3
- .image-saver {
3
+ .image-asset {
4
4
  &.field {
5
5
  .value {
6
6
  border-bottom: 0;
@@ -2,7 +2,7 @@
2
2
  @import "./fieldset";
3
3
  @import "./resize-sensor";
4
4
  @import "./overlay";
5
- @import "./image-saver";
5
+ @import "./image-asset";
6
6
  @import "./throbber";
7
7
  @import "lanes/vendor/styles/toggle";
8
8
  @import "lanes/vendor/styles/widgets";
@@ -1,10 +1,15 @@
1
1
  class Change extends Lanes.React.Component
2
2
 
3
3
  renderField: (field) ->
4
+ unless _.isObject(field.from)
5
+ from = <div className="from">{field.from}</div>
6
+ unless _.isObject(field.to)
7
+ to = <div className='to'>{field.to}</div>
8
+
4
9
  <div className='change' key={field.name}>
5
10
  <div className="field">{@model.record_name} {_.field2title field.name}:</div>
6
- <div className="from">{field.from}</div>
7
- <div className='to'>{field.to}</div>
11
+ {from}
12
+ {to}
8
13
  </div>
9
14
 
10
15
  render: ->
@@ -12,17 +17,15 @@ class Change extends Lanes.React.Component
12
17
  if @model.by?.email
13
18
  <a href={"mailto:#{@model.by.email}"}>{@model.by.name}</a>
14
19
  else if @model?.by.name
15
- @model.by.name
20
+ <span>@model.by.name</span>
16
21
  else
17
- 'Unknown User'
18
-
19
- user =
20
- <LC.Tooltip id='user-email' placement='left'
21
- content={Lanes.Vendor.Moment( @model.created_at ).fromNow()}
22
- >{user}</LC.Tooltip>
22
+ <span>Unknown User</span>
23
23
 
24
24
  <div className="update">
25
- {user}
25
+ <div className="header">
26
+ {user}
27
+ <span className="time">{Lanes.Vendor.Moment( @model.created_at ).fromNow()}</span>
28
+ </div>
26
29
  <div className="changes">
27
30
  {@renderField(change) for change in @model.displayed_changes }
28
31
  </div>
@@ -24,7 +24,20 @@
24
24
  .dropdown-menu {
25
25
  .update {
26
26
  border-bottom: 1px solid $table-border-color;
27
- padding: 0.75rem;
27
+ margin-bottom: 5px;
28
+ padding: 5px;
29
+ &:last-child {
30
+ margin-bottom: 0;
31
+ border-bottom: 0;
32
+ }
33
+ .header {
34
+ display: flex;
35
+ align-items: stretch;
36
+ justify-content: space-between;
37
+ }
38
+ .time {
39
+ font-size: 80%;
40
+ }
28
41
  .change > div {
29
42
  margin-bottom: 2px;
30
43
  }
@@ -1,8 +1,12 @@
1
1
  class BaseExtension
2
2
 
3
- @extended: (klass) ->
3
+ @afterExtended: (klass) ->
4
4
  Lanes.Extensions.register(klass)
5
5
 
6
+ title: ->
7
+ _.titleize @identifier
8
+
9
+
6
10
  Lanes.Extensions.Base = Lanes.lib.MakeBaseClass(
7
11
  Lanes.Vendor.Ampersand.State, BaseExtension
8
12
  )
@@ -0,0 +1,81 @@
1
+ class Lanes.Models.Asset extends Lanes.Models.Base
2
+
3
+ props:
4
+ id: 'integer'
5
+ order: 'integer'
6
+ thumbnail: 'object'
7
+ medium: 'object'
8
+ original: 'object'
9
+ metadata: 'object'
10
+
11
+ session:
12
+ data: 'string'
13
+ blob: 'object'
14
+ owner: 'object'
15
+ parent: 'object'
16
+ parent_association: 'string'
17
+
18
+ derived:
19
+ original_url:
20
+ deps: ['data', 'original'], fn: ->
21
+ if @data then @data else @original?.url
22
+ medium_url:
23
+ deps: ['data', 'medium'], fn: ->
24
+ if @data then @data else @medium?.url
25
+ thumbnail_url:
26
+ deps: ['data', 'thumbnail'], fn: ->
27
+ if @data then @data else @thumbnail?.url
28
+
29
+ hasImage:
30
+ deps: ['data', 'original', 'metadata'], fn: ->
31
+ @metadata?.content_type?.includes?('image')
32
+ events:
33
+ 'change:blob': 'onBlobChange'
34
+ 'parent:save': 'onParentSave'
35
+
36
+ initialize: ->
37
+ @on('change:parent', =>
38
+ oldParent = _this. previousAttributes().parent
39
+ @stopListening(oldParent, 'save', @save) if oldParent
40
+ @listenTo(@parent, 'save', @save)
41
+ )
42
+
43
+
44
+ onBlobChange: ->
45
+ if @blob
46
+ @metadata ||= {}
47
+ @metadata.content_type = @blob.type
48
+ reader = new FileReader()
49
+ reader.onloadend = (ev) =>
50
+ @data = reader.result if ev.type is 'loadend'
51
+ reader.readAsDataURL(@blob)
52
+
53
+ else
54
+ @data = null
55
+ @mdatadata?.content_type = undefined
56
+
57
+
58
+ save: ->
59
+ return unless @blob
60
+
61
+ form = new FormData()
62
+ form.append("id", @id) if @id
63
+ form.append("file", @blob, @blob.name)
64
+ form.append("owner_type", _.classify(@parent.modelTypeIdentifier()))
65
+ form.append("owner_id", @parent.getId())
66
+ form.append("owner_association", @parent_association)
67
+ form.append("extension_id", @parent.extensionIdentifier())
68
+
69
+ url = Lanes.config.api_path + '/asset'
70
+
71
+ Lanes.Vendor.xhr.post(url, {body: form}, (err, resp, body) =>
72
+ @blob = null
73
+ if err
74
+ @errors = { http: err.message }
75
+ else
76
+ reply = JSON.parse(body)
77
+ if reply.errors or reply.success is false
78
+ @errors = reply.errors
79
+ else
80
+ @set( reply.data )
81
+ )
@@ -36,7 +36,9 @@ class Lanes.Models.AssocationMap
36
36
 
37
37
  replace: (parent, name, model) ->
38
38
  parent._cache[name] = model
39
- model.parent = parent
39
+ model.parent = parent if model.hasAttribute?('parent') or model.parent?
40
+ model.parent_association = name if model.hasAttribute?('parent_association') or model.parent_association?
41
+
40
42
  parent.trigger("change", parent, {})
41
43
  parent.trigger("change:#{name}", model, {})
42
44
 
@@ -58,8 +60,12 @@ class Lanes.Models.AssocationMap
58
60
  else
59
61
  new target_class(options)
60
62
  else
61
- Proxy = association.getProxyFor(name)
62
- new Proxy( association, association.getOptions(name, @) )
63
+ existing = this._cache[name]
64
+ if existing and not existing.isProxy and existing[fk] == @[pk]
65
+ existing
66
+ else
67
+ Proxy = association.getProxyFor(name)
68
+ new Proxy( association, association.getOptions(name, @) )
63
69
 
64
70
  # will be called in the scope of the parent model
65
71
  createCollection: (association, name, definition, fk, pk, target_class) ->
@@ -98,7 +104,8 @@ class Lanes.Models.AssocationMap
98
104
  -> definition.default.apply(this, args) || defaultCreator.apply(this, args)
99
105
  else
100
106
  defaultCreator
101
- { fn: _.partial(createFn, args...) }
107
+ { fn: _.partial(createFn, args...), deps: [ @pk(name) ] }
108
+
102
109
 
103
110
  # Sets the assocations for "model"
104
111
  set: (model, data, options) ->
@@ -107,9 +114,11 @@ class Lanes.Models.AssocationMap
107
114
  if @exists(name) && Lanes.u.isModel(value) && !value.isNew()
108
115
  model[@pk(name)] = if value then value.getId() else null
109
116
 
110
- setFromServer: (model, data, options) ->
117
+ setFromServer: (model, data, options, method) ->
111
118
  this._set(model, data, options, 'setFromServer')
112
119
 
120
+
121
+
113
122
  onIdChange: (model) ->
114
123
  for name, def of @definitions
115
124
  if def.collection and @isCreated(model, name) and model[name]?.associationFilter
@@ -14,10 +14,11 @@ ProxyMethods = {
14
14
 
15
15
  replaceWithModel: (model, options, whenReadyMethod) ->
16
16
  model = new @_proxied_model(model) unless Lanes.u.isModel(model)
17
-
18
- @_proxied_parent?.replace(
19
- @_proxied_options.parent, options.association_name, model
20
- )
17
+ if @_proxied_parent
18
+ if @_proxied_parent.isProxy
19
+ @_proxied_parent.replaceProxy(options.association_name, model)
20
+ else
21
+ @_proxied_parent.replace( @_proxied_options.parent, options.association_name, model )
21
22
  relayEvents(@_proxied_events, model, 'on')
22
23
  relayEvents(@_proxied_once_events, model, 'once')
23
24
 
@@ -32,9 +33,11 @@ ProxyMethods = {
32
33
 
33
34
  _replaceAndCall: (options, fn) ->
34
35
  model = new @_proxied_model()
36
+ retVal = undefined
35
37
  @replaceWithModel(model, options, ->
36
- fn.call(model, fn)
38
+ retVal = fn.call(model, fn)
37
39
  )
40
+ retVal
38
41
 
39
42
 
40
43
  serialize: (options) ->
@@ -100,7 +103,7 @@ Lanes.Models.AssocationProxy = {
100
103
  Proxy = Lanes.Models.AssocationProxy.construct(klass,
101
104
  association_name: name
102
105
  )
103
- new Proxy(@)
106
+ new Proxy(@, @_proxied_model::associations.getOptions(name, @))
104
107
  )
105
108
 
106
109