lanes 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/client/lanes/Config.coffee +3 -1
- data/client/lanes/access/screens/user-management/UserManagement.cjsx +2 -2
- data/client/lanes/components/grid/Body.cjsx +8 -1
- data/client/lanes/components/grid/EditingMixin.cjsx +5 -2
- data/client/lanes/components/grid/Grid.cjsx +1 -1
- data/client/lanes/components/grid/PopOverMixin.cjsx +1 -1
- data/client/lanes/components/grid/row-editor.scss +6 -0
- data/client/lanes/components/grid/styles.scss +1 -1
- data/client/lanes/components/record-finder/RecordFinder.cjsx +22 -16
- data/client/lanes/components/select-field/SelectField.cjsx +20 -27
- data/client/lanes/components/shared/DateTime.cjsx +12 -23
- data/client/lanes/components/shared/DisplayValue.cjsx +5 -10
- data/client/lanes/components/shared/FieldMixin.cjsx +107 -56
- data/client/lanes/components/shared/FieldWrapper.cjsx +59 -6
- data/client/lanes/components/shared/FormGroup.cjsx +3 -8
- data/client/lanes/components/shared/Helpers.coffee +1 -1
- data/client/lanes/components/shared/Icon.cjsx +1 -1
- data/client/lanes/components/shared/ImageAsset.cjsx +46 -0
- data/client/lanes/components/shared/Input.cjsx +5 -1
- data/client/lanes/components/shared/InputFieldMixin.cjsx +8 -27
- data/client/lanes/components/shared/NumberInput.cjsx +11 -4
- data/client/lanes/components/shared/RadioField.cjsx +18 -8
- data/client/lanes/components/shared/ScreenWrapper.cjsx +1 -1
- data/client/lanes/components/shared/TextArea.cjsx +15 -0
- data/client/lanes/components/shared/ToggleField.cjsx +11 -19
- data/client/lanes/components/shared/fields.scss +22 -76
- data/client/lanes/components/shared/{image-saver.scss → image-asset.scss} +1 -1
- data/client/lanes/components/shared/styles.scss +1 -1
- data/client/lanes/components/toolbar/RemoteChangeSets.cjsx +13 -10
- data/client/lanes/components/toolbar/changes-notification.scss +14 -1
- data/client/lanes/extension/Base.coffee +5 -1
- data/client/lanes/models/Asset.coffee +81 -0
- data/client/lanes/models/AssociationMap.coffee +14 -5
- data/client/lanes/models/AssociationProxy.coffee +9 -6
- data/client/lanes/models/Base.coffee +6 -3
- data/client/lanes/models/Collection.coffee +1 -3
- data/client/lanes/models/JobStatus.coffee +3 -0
- data/client/lanes/models/Query.coffee +3 -2
- data/client/lanes/models/Sync.coffee +6 -4
- data/client/lanes/react/mixins/Access.coffee +1 -1
- data/client/lanes/react/mixins/Data.coffee +33 -31
- data/client/lanes/screens/Commands.coffee +0 -1
- data/client/lanes/screens/Definitions.coffee +1 -1
- data/client/lanes/screens/SystemSettings.cjsx +23 -11
- data/client/lanes/screens/UserPreferences.cjsx +3 -2
- data/client/lanes/styles/bootstrap-custom-grid.scss +6 -4
- data/client/lanes/styles/fonts.scss +9 -0
- data/client/lanes/styles/global/styles.scss +1 -0
- data/client/lanes/styles/variables.scss +1 -1
- data/client/lanes/testing/Helpers.coffee +5 -3
- data/client/lanes/vendor/development/base.js +17206 -19211
- data/client/lanes/vendor/development/calendar.js +67 -471
- data/client/lanes/vendor/development/commons.js +21680 -19814
- data/client/lanes/vendor/development/helpers.js +40 -29
- data/client/lanes/vendor/development/toggle.js +22 -19
- data/client/lanes/vendor/development/widgets.js +2476 -2625
- data/client/lanes/vendor/production/base.js +19034 -21038
- data/client/lanes/vendor/production/calendar.js +67 -471
- data/client/lanes/vendor/production/commons.js +21369 -19136
- data/client/lanes/vendor/production/toggle.js +22 -19
- data/client/lanes/vendor/production/widgets.js +2476 -2625
- data/client/lanes/vendor/styles/widgets.scss +2 -0
- data/client/lanes/workspace/FirstRun.cjsx +69 -0
- data/client/lanes/workspace/Navbar.cjsx +4 -2
- data/client/lanes/workspace/ScreenView.cjsx +9 -1
- data/client/lanes/workspace/index.js +1 -0
- data/client/lanes/workspace/styles/screens.scss +11 -0
- data/config/database.yml +2 -2
- data/db/migrate/01_create_system_settings.rb +0 -1
- data/db/migrate/02_create_assets.rb +15 -0
- data/lanes.gemspec +25 -28
- data/lib/lanes/access/authentication_provider.rb +20 -7
- data/lib/lanes/access/role_collection.rb +9 -2
- data/lib/lanes/api/default_routes.rb +4 -4
- data/lib/lanes/api/formatted_reply.rb +15 -11
- data/lib/lanes/api/handlers/asset.rb +37 -0
- data/lib/lanes/api/request_wrapper.rb +18 -4
- data/lib/lanes/api/routing.rb +17 -6
- data/lib/lanes/api/updates.rb +1 -1
- data/lib/lanes/api.rb +1 -1
- data/lib/lanes/asset.rb +38 -0
- data/lib/lanes/concerns/all.rb +1 -1
- data/lib/lanes/concerns/asset_uploader.rb +60 -0
- data/lib/lanes/configuration.rb +1 -2
- data/lib/lanes/extension.rb +1 -1
- data/lib/lanes/logger.rb +26 -16
- data/lib/lanes/system_settings.rb +13 -8
- data/lib/lanes/version.rb +1 -1
- data/lib/lanes.rb +1 -0
- data/npm-build/base.js +1 -1
- data/npm-build/package.json +9 -9
- data/spec/command-reference-files/initial/Gemfile +1 -1
- data/spec/fixtures/logo.png +0 -0
- data/spec/lanes/components/grid/PopoverEditorSpec.coffee +48 -0
- data/spec/server/asset_spec.rb +34 -0
- data/spec/server/spec_helper.rb +14 -2
- metadata +118 -127
- data/client/lanes/components/shared/ControlLabel.cjsx +0 -45
- data/client/lanes/components/shared/ImageSaver.cjsx +0 -33
- data/client/lanes/models/SystemSettings.coffee +0 -0
- data/client/lanes/models/mixins/FileSupport.coffee +0 -60
- data/lib/lanes/api/handlers/file.rb +0 -26
- data/lib/lanes/concerns/image_uploader.rb +0 -42
@@ -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,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: (
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
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
|
-
|
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
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
+
/>
|
@@ -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
|
-
|
4
|
+
fieldClassName: 'toggle'
|
5
5
|
|
6
|
-
|
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
|
-
|
13
|
-
@props.model[@props.name] = ev.target.checked
|
14
|
-
null
|
17
|
+
renderEdit: (props) ->
|
15
18
|
|
16
|
-
|
17
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
font-weight: normal;
|
13
|
-
width: 100%;
|
14
|
-
}
|
6
|
+
|
7
|
+
|
8
|
+
.lanes-field {
|
9
|
+
|
10
|
+
.control-label {display: block;}
|
11
|
+
|
15
12
|
&.display {
|
16
|
-
.
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
&.
|
39
|
-
|
40
|
-
|
41
|
-
cursor: not-allowed;
|
28
|
+
&.align-center{
|
29
|
+
text-align: center;
|
30
|
+
.control-label, .value, .rw-widget input { text-align: center; }
|
42
31
|
}
|
43
|
-
|
44
|
-
|
45
|
-
|
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,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
|
-
|
7
|
-
|
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
|
-
|
20
|
+
<span>@model.by.name</span>
|
16
21
|
else
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
|
62
|
-
|
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
|
-
|
19
|
-
|
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
|
|