This commit is contained in:
Nick Winter 2014-04-18 09:53:34 -07:00
commit 848ebf2ffe
39 changed files with 472 additions and 197 deletions

View file

@ -15,6 +15,9 @@
fork: "Fork"
play: "Play"
retry: "Retry"
watch: "Watch"
unwatch: "Unwatch"
submit_patch: "Submit Patch"
units:
second: "second"

View file

@ -60,6 +60,7 @@ class CocoModel extends Backbone.Model
return result.errors unless result.valid
save: (attrs, options) ->
@set 'editPath', document.location.pathname
options ?= {}
success = options.success
options.success = (resp) =>
@ -68,7 +69,6 @@ class CocoModel extends Backbone.Model
@markToRevert()
@clearBackup()
@trigger "save", @
patch.setStatus 'accepted' for patch in @acceptedPatches or []
return super attrs, options
fetch: ->
@ -95,7 +95,6 @@ class CocoModel extends Backbone.Model
cloneNewMinorVersion: ->
newData = $.extend(null, {}, @attributes)
clone = new @constructor(newData)
clone.acceptedPatches = @acceptedPatches
clone
cloneNewMajorVersion: ->
@ -221,9 +220,11 @@ class CocoModel extends Backbone.Model
delta = @getDelta()
deltasLib.expandDelta(delta, @_revertAttributes, @schema())
addPatchToAcceptOnSave: (patch) ->
@acceptedPatches ?= []
@acceptedPatches.push patch
@acceptedPatches = _.uniq(@acceptedPatches, false, (p) -> p.id)
watch: (doWatch=true) ->
$.ajax("#{@urlRoot}/#{@id}/watch", {type:'PUT', data:{on:doWatch}})
@watching = -> doWatch
watching: ->
return me.id in (@get('watchers') or [])
module.exports = CocoModel

View file

@ -5,4 +5,7 @@ module.exports = class PatchModel extends CocoModel
urlRoot: "/db/patch"
setStatus: (status) ->
$.ajax("/db/patch/#{@id}/status", {type:"PUT", data: {status:status}})
PatchModel.setStatus @id, status
@setStatus: (id, status) ->
$.ajax("/db/patch/#{id}/status", {type:"PUT", data: {status:status}})

View file

@ -63,7 +63,7 @@ patchableProps = ->
status: { enum: ['pending', 'accepted', 'rejected', 'cancelled']}
})
allowPatches: { type: 'boolean' }
listeners: me.array({title:'Listeners'},
watchers: me.array({title:'Watchers'},
me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]))
me.extendPatchableProperties = (schema) ->

View file

@ -265,3 +265,8 @@ body[lang='ja']
font-family: 'Glyphicons Halflings'
src: url("/fonts/glyphicons-halflings-regular.eot")
src: url("/fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("/fonts/glyphicons-halflings-regular.woff") format("woff"), url("/fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("/fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular") format("svg")
.spr:after
content: " "
.spl:before
content: " "

View file

@ -1,4 +1,11 @@
#editor-level-component-edit-view
nav
margin-bottom: 0
#component-patches
padding: 0 10px 10px
background: white
.navbar-text
float: left
@ -7,9 +14,13 @@
left: 0
right: 0
bottom: 0
top: 50px
top: 35px
border: 2px solid black
border-top: none
.active > a, .active > a:hover, .active > a:focus
background-color: white !important
.inner-editor
position: absolute
left: 0
right: 0
bottom: 0
top: 0px

View file

@ -1,4 +1,6 @@
#editor-level-components-tab-view
h3
margin-top: 0
.components-container
position: absolute
@ -7,7 +9,7 @@
.treema-root
position: absolute
top: 50px
top: 35px
bottom: 0
width: 250px
overflow: scroll
@ -25,13 +27,13 @@
.treema-root
position: absolute
top: 50px
top: 35px
right: 0
left: 0px
bottom: 0
overflow: scroll
#create-new-component-button
#create-new-component-button-no-select
position: absolute
top: 0
right: 0

View file

@ -1,10 +1,10 @@
#editor-level-view
&, #level-editor-top-nav
min-width: 1024px
a
font-family: helvetica, arial, sans serif
#top-nav
display: none
position: absolute
top: 0px
left: 0px
@ -12,22 +12,54 @@
bottom: 0px
$BG: rgba(228, 207, 140, 1.0)
$NAVBG: #2f261d
li.navbar-btn
margin-right: 5px
#level-editor-top-nav
.nav-tabs
border-bottom: 0 !important
.active > a, .active > a:hover, .active > a:focus
background-color: $BG !important
border-color: darken($BG, 50%)
border-bottom: 0
// custom navbar height rules
.navbar-nav > li > a
padding: 7px 8px 8px
cursor: pointer
&:hover
background-color: lighten($NAVBG, 10%)
.navbar
min-height: 0px
border-radius: 0
.navbar-right // not sure why bootstrap puts a big negative margin in, but this overrides it
margin-right: 10px
// custom navbar styling
.navbar-brand
padding-top: 7px
padding-bottom: 7px
color: lighten(gold, 30%)
.navbar-header
border-left: 2px solid lighten($NAVBG, 20%)
border-right: 2px solid lighten($NAVBG, 20%)
background: lighten($NAVBG, 10%)
margin-left: 20px
.nav-tabs
margin-left: 5px
border-bottom: 0 !important
.active > a, .active > a:hover, .active > a:focus
background-color: $BG !important
border-color: darken($BG, 50%)
border-bottom: 0
a
padding: 7px 5px !important
.dropdown-menu a
cursor: pointer
&:hover
background-color: #d3d3d3
.badge
background-color: green
.outer-content
background-color: $BG
position: absolute
top: 0
top: 35px
bottom: 0
left: 0
right: 0
@ -45,12 +77,11 @@
#level-editor-tabs
position: absolute
left: 20px
right: 20px
top: 66px
bottom: 20px
left: 15px
right: 15px
top: 15px
bottom: 15px
.treema-root
background-color: white
border-radius: 4px
border-radius: 4px

View file

@ -7,9 +7,13 @@
left: 0
right: 0
bottom: 0
top: 50px
top: 35px
border: 2px solid black
border-top: none
.active > a, .active > a:hover, .active > a:focus
background-color: white !important
.inner-editor
position: absolute
left: 0
right: 0
bottom: 0
top: 0px

View file

@ -1,4 +1,6 @@
#editor-level-systems-tab-view
h3
margin-top: 0
.systems-container
position: absolute
@ -7,7 +9,7 @@
.treema-root
position: absolute
top: 50px
top: 35px
bottom: 0
width: 250px
overflow: scroll
@ -30,7 +32,7 @@
.treema-root
position: absolute
top: 50px
top: 35px
right: 0
left: 0px
bottom: 0

View file

@ -1,3 +1,3 @@
.patches-view
.status-buttons
margin: 10px 0
margin-bottom: 10px

View file

@ -1,23 +1,44 @@
nav.navbar.navbar-default(role='navigation')
.container-fluid
.navbar-header
span.navbar-brand
span(data-i18n="editor.level_component_edit_title")
| Edit Component
span :
| "#{editTitle}"
.collapse.navbar-collapse
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#component-code" data-toggle="tab" data-i18n="general.code") Code
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#component-code" data-toggle="tab" data-i18n="general.code")#component-code-tab Code
li
a(href="#component-config-schema" data-toggle="tab" data-i18n="editor.level_component_config_schema")#component-config-schema-tab Config Schema
li
a(href="#component-settings" data-toggle="tab" data-i18n="editor.level_component_settings")#component-settings-tab Settings
li
a(href="#component-patches" data-toggle="tab" data-i18n="resources.patches")#component-patches-tab Patches
.navbar-header
span.navbar-brand= editTitle
ul.nav.navbar-nav.navbar-right
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li
a(href="#component-config-schema" data-toggle="tab" data-i18n="editor.level_component_config_schema") Config Schema
li
a(href="#component-settings" data-toggle="tab" data-i18n="editor.level_component_settings") Settings
ul.nav.navbar-nav.navbar-left
li(data-i18n="general.version_history").btn.btn-primary.navbar-btn#history-button Version History
ul.nav.navbar-nav.navbar-right
li(data-i18n="editor.level_component_btn_new").btn.btn-primary.navbar-btn#create-new-component-button Create New Component
a#component-watch-button
span.watch
span.glyphicon.glyphicon-eye-open
span.spl Watch
span.unwatch.secret
span.glyphicon.glyphicon-eye-close
span.spl Unwatch
li#patch-component-button
a(data-i18n="common.submit_patch") Submit Patch
li#create-new-component-button
a(data-i18n="editor.level_component_btn_new") Create New Component
li.divider
li.dropdown-header Info
li#component-history-button
a(data-i18n="general.version_history") Version History
.tab-content
.tab-pane.active#component-code
@ -26,3 +47,5 @@ nav.navbar.navbar-default(role='navigation')
#config-schema-treema
.tab-pane#component-settings
#edit-component-treema
.tab-pane#component-patches
.patches-view

View file

@ -4,6 +4,6 @@
.edit-component-container
if me.isAdmin()
button(data-i18n="editor.level_component_btn_new").btn.btn-primary#create-new-component-button Create New Component
button(data-i18n="editor.level_component_btn_new").btn.btn-primary#create-new-component-button-no-select Create New Component
#editor-level-component-edit-view

View file

@ -1,74 +1,90 @@
extends /templates/base
block outer_content
.outer-content
block header
if level.loading
nav.navbar.navbar-default(role='navigation')#level-editor-top-nav
.container-fluid
ul.nav.navbar-nav
li
a(href="/editor/level", data-i18n="editor.back") Back
.navbar-header
span.navbar-brand
span(data-i18n="editor.level_title") Level Editor
span :
span.level-title #{level.attributes.name}
.collapse.navbar-collapse
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#editor-level-thangs-tab-view", data-toggle="tab", data-i18n="editor.level_tab_thangs") Thangs
li
a(href="#editor-level-scripts-tab-view", data-toggle="tab", data-i18n="editor.level_tab_scripts") Scripts
li
a(href="#editor-level-settings-tab-view", data-toggle="tab", data-i18n="editor.level_tab_settings") Settings
li
a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components") Components
li
a(href="#editor-level-systems-tab-view", data-toggle="tab", data-i18n="editor.level_tab_systems") Systems
li
a(href="#editor-level-patches", data-toggle="tab", data-i18n="resources.patches")#patches-tab Patches
ul.nav.navbar-nav.navbar-right
li(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert", disabled=authorized === true ? undefined : "true").btn.btn-primary.navbar-btn#revert-button Revert
if authorized
li(data-i18n="common.save").btn.btn-primary.navbar-btn#commit-level-start-button Save
else
li(data-i18n="common.patch").btn.btn-primary.navbar-btn#commit-level-patch-button Patch
li(data-i18n="common.fork", disabled=anonymous ? "true": undefined).btn.btn-primary.navbar-btn#fork-level-start-button Fork
li(title="⌃↩ or ⌘↩: Play preview of current level", data-i18n="common.play")#play-button.btn.btn-inverse.banner.navbar-btn Play!
li.divider
li.dropdown
a.dropdown-toggle(href='#', data-toggle='dropdown', data-i18n="editor.more")
| More
b.caret
ul.dropdown-menu
li#history-button
a(href='#', data-i18n="general.version_history") Version History
li
a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home', data-i18n="editor.wiki") Wiki
li
a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat") Live Chat
li
a(href='http://discourse.codecombat.com/category/artisan', data-i18n="nav.forum") Forum
li
a(data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Email
ul.dropdown-menu
li
span(data-i18n="editor.level_some_options").dropdown-menu-header Some Options?
li.divider
li
a(data-delay="1000", href="#", data-i18n="common.delay_1_sec") 1 second
a(data-delay="3000", href="#", data-i18n="common.delay_3_sec") 3 seconds
a(data-delay="5000", href="#", data-i18n="common.delay_5_sec") 5 seconds
a(data-delay="90019001", href="#", data-i18n="common.manual") Manual
a(href="/editor/level")
span.glyphicon-home.glyphicon
else
nav.navbar.navbar-default(role='navigation')#level-editor-top-nav
ul.nav.navbar-nav
li
a(href="/editor/level")
span.glyphicon-home.glyphicon
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#editor-level-thangs-tab-view", data-toggle="tab", data-i18n="editor.level_tab_thangs") Thangs
li
a(href="#editor-level-scripts-tab-view", data-toggle="tab", data-i18n="editor.level_tab_scripts") Scripts
li
a(href="#editor-level-settings-tab-view", data-toggle="tab", data-i18n="editor.level_tab_settings") Settings
li
a(href="#editor-level-components-tab-view", data-toggle="tab", data-i18n="editor.level_tab_components") Components
li
a(href="#editor-level-systems-tab-view", data-toggle="tab", data-i18n="editor.level_tab_systems") Systems
li
a(href="#editor-level-patches", data-toggle="tab")#patches-tab
span(data-i18n="resources.patches").spr Patches
- var patches = level.get('patches')
if patches && patches.length
span.badge= patches.length
.navbar-header
span.navbar-brand #{level.attributes.name}
ul.nav.navbar-nav.navbar-right
if authorized
li#commit-level-start-button
a
span.glyphicon-floppy-disk.glyphicon
else
li#level-patch-button
a
span.glyphicon-floppy-disk.glyphicon
li(title="⌃↩ or ⌘↩: Play preview of current level")#play-button
a
span.glyphicon-play.glyphicon
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li
a#level-watch-button
span.watch
span.glyphicon.glyphicon-eye-open
span.spl Watch
span.unwatch.secret
span.glyphicon.glyphicon-eye-close
span.spl Unwatch
li(class=anonymous ? "disabled": "")
a(data-i18n="common.fork")#fork-level-start-button Fork
li(class=anonymous ? "disabled": "")
a(data-toggle="coco-modal", data-target="modal/revert", data-i18n="editor.revert")#revert-button Revert
li.divider
li.dropdown-header Info
li#level-history-button
a(href='#', data-i18n="general.version_history") Version History
li.divider
li.dropdown-header Help
li
a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home', data-i18n="editor.wiki", target="_blank") Wiki
li
a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat", target="_blank") Live Chat
li
a(href='http://discourse.codecombat.com/category/artisan', data-i18n="nav.forum", target="_blank") Forum
li
a(data-toggle="coco-modal", data-target="modal/contact", data-i18n="nav.contact") Email
block outer_content
.outer-content
div.tab-content#level-editor-tabs
div.tab-pane.active#editor-level-thangs-tab-view

View file

@ -1,21 +1,40 @@
nav.navbar.navbar-default(role='navigation')
.container-fluid
.navbar-header
span.navbar-brand
span(data-i18n="editor.level_system_edit_title")
| Edit System
span :
| "#{editTitle}"
.collapse.navbar-collapse
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#system-code" data-toggle="tab") Code
ul.nav.navbar-nav.nav-tabs
li.active
a(href="#system-code" data-toggle="tab")#system-code-tab Code
li
a(href="#system-config-schema" data-toggle="tab")#system-config-schema-tab Config Schema
li
a(href="#system-settings" data-toggle="tab")#system-settings-tab Settings
li
a(href="#system-patches" data-toggle="tab" data-i18n="resources.patches")#system-patches-tab Patches
ul.nav.navbar-nav.navbar-right
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li
a(href="#system-config-schema" data-toggle="tab") Config Schema
li
a(href="#system-settings" data-toggle="tab") Settings
ul.nav.navbar-nav.navbar-right
li(data-i18n="editor.level_system_btn_new").btn.btn-primary.navbar-btn#create-new-system-button Create New System
a#system-watch-button
span.watch
span.glyphicon.glyphicon-eye-open
span.spl Watch
span.unwatch.secret
span.glyphicon.glyphicon-eye-close
span.spl Unwatch
li#patch-system-button
a(data-i18n="common.submit_patch") Submit Patch
li#create-new-system
a(data-i18n="editor.level_system_btn_new") Create New System
li.divider
li.dropdown-header Info
li#system-history-button
a(data-i18n="general.version_history") Version History
.navbar-header
span.navbar-brand= editTitle
.tab-content
.tab-pane.active#system-code
@ -24,3 +43,5 @@ nav.navbar.navbar-default(role='navigation')
#config-schema-treema
.tab-pane#system-settings
#edit-system-treema
.tab-pane#system-patches
.patches-view

View file

@ -6,6 +6,7 @@ block modal-header-content
block modal-body-content
.modal-body
p= patch.get('commitMessage')
.changes-stub

View file

@ -8,7 +8,7 @@ block modal-header-content
block modal-body-content
if dataList
table.table
table.table.table-condensed
tr
th(data-i18n="general.name") Name
th(data-i18n="general.version") Version

View file

@ -21,12 +21,12 @@ module.exports = class DeltaView extends CocoView
if @headModel
@headDeltas = @headModel.getExpandedDelta()
@conflicts = deltasLib.getConflicts(@headDeltas, @expandedDeltas)
DeltaView.deltaCounter += @expandedDeltas.length
getRenderData: ->
c = super()
c.deltas = @expandedDeltas
c.counter = DeltaView.deltaCounter
DeltaView.deltaCounter += @expandedDeltas.length
c
afterRender: ->

View file

@ -1,7 +1,9 @@
View = require 'views/kinds/CocoView'
VersionHistoryView = require 'views/editor/component/versions_view'
template = require 'templates/editor/level/component/edit'
LevelComponent = require 'models/LevelComponent'
VersionHistoryView = require 'views/editor/component/versions_view'
PatchesView = require 'views/editor/patches_view'
SaveVersionModal = require 'views/modal/save_version_modal'
module.exports = class LevelComponentEditView extends View
id: "editor-level-component-edit-view"
@ -10,8 +12,14 @@ module.exports = class LevelComponentEditView extends View
events:
'click #done-editing-component-button': 'endEditing'
'click #history-button': 'showVersionHistory'
'click .nav a': (e) -> $(e.target).tab('show')
'click #component-patches-tab': -> @patchesView.load()
'click #component-code-tab': 'buildCodeEditor'
'click #component-config-schema-tab': 'buildConfigSchemaTreema'
'click #component-settings-tab': 'buildSettingsTreema'
'click #component-history-button': 'showVersionHistory'
'click #patch-component-button': 'startPatchingComponent'
'click #component-watch-button': 'toggleWatchComponent'
constructor: (options) ->
super options
@ -21,6 +29,7 @@ module.exports = class LevelComponentEditView extends View
getRenderData: (context={}) ->
context = super(context)
context.editTitle = "#{@levelComponent.get('system')}.#{@levelComponent.get('name')}"
context.component = @levelComponent
context
afterRender: ->
@ -28,6 +37,8 @@ module.exports = class LevelComponentEditView extends View
@buildSettingsTreema()
@buildConfigSchemaTreema()
@buildCodeEditor()
@patchesView = @insertSubView(new PatchesView(@levelComponent), @$el.find('.patches-view'))
@$el.find('#component-watch-button').find('> span').toggleClass('secret') if @levelComponent.watching()
buildSettingsTreema: ->
data = _.pick @levelComponent.attributes, (value, key) => key in @editableSettings
@ -40,7 +51,6 @@ module.exports = class LevelComponentEditView extends View
schema: schema
data: data
callbacks: {change: @onComponentSettingsEdited}
treemaOptions.readOnly = true unless me.isAdmin()
@componentSettingsTreema = @$el.find('#edit-component-treema').treema treemaOptions
@componentSettingsTreema.build()
@componentSettingsTreema.open()
@ -58,7 +68,6 @@ module.exports = class LevelComponentEditView extends View
schema: LevelComponent.schema.properties.configSchema
data: @levelComponent.get 'configSchema'
callbacks: {change: @onConfigSchemaEdited}
treemaOptions.readOnly = true unless me.isAdmin()
@configSchemaTreema = @$el.find('#config-schema-treema').treema treemaOptions
@configSchemaTreema.build()
@configSchemaTreema.open()
@ -70,10 +79,10 @@ module.exports = class LevelComponentEditView extends View
Backbone.Mediator.publish 'level-component-edited', levelComponent: @levelComponent
buildCodeEditor: ->
editorEl = @$el.find '#component-code-editor'
editorEl.text @levelComponent.get('code')
@editor?.destroy()
editorEl = $('<div></div>').text(@levelComponent.get('code')).addClass('inner-editor')
@$el.find('#component-code-editor').empty().append(editorEl)
@editor = ace.edit(editorEl[0])
@editor.setReadOnly(not me.isAdmin())
session = @editor.getSession()
session.setMode 'ace/mode/coffee'
session.setTabSize 2
@ -90,11 +99,21 @@ module.exports = class LevelComponentEditView extends View
Backbone.Mediator.publish 'level-component-editing-ended', levelComponent: @levelComponent
null
showVersionHistory: (e) ->
versionHistoryView = new VersionHistoryView {}, @levelComponent.id
@openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e
startPatchingComponent: (e) ->
@openModalView new SaveVersionModal({model:@levelComponent})
Backbone.Mediator.publish 'level:view-switched', e
toggleWatchComponent: ->
button = @$el.find('#component-watch-button')
@levelComponent.watch(button.find('.watch').is(':visible'))
button.find('> span').toggleClass('secret')
destroy: ->
@editor?.destroy()
super()
showVersionHistory: (e) ->
versionHistoryView = new VersionHistoryView component:@levelComponent, @levelComponent.id
@openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e

View file

@ -21,6 +21,7 @@ module.exports = class ComponentsTabView extends View
events:
'click #create-new-component-button': 'createNewLevelComponent'
'click #create-new-component-button-no-select': 'createNewLevelComponent'
onLevelThangsChanged: (e) ->
thangsData = e.thangsData

View file

@ -27,10 +27,18 @@ module.exports = class EditorLevelView extends View
'click #play-button': 'onPlayLevel'
'click #commit-level-start-button': 'startCommittingLevel'
'click #fork-level-start-button': 'startForkingLevel'
'click #history-button': 'showVersionHistory'
'click #level-history-button': 'showVersionHistory'
'click #patches-tab': -> @patchesView.load()
'click #commit-level-patch-button': 'startPatchingLevel'
'click #level-patch-button': 'startPatchingLevel'
'click #level-watch-button': 'toggleWatchLevel'
subscriptions:
'refresh-level-editor': 'rerenderAllViews'
rerenderAllViews: ->
for view in [@thangsTab, @settingsTab, @scriptsTab, @componentsTab, @systemsTab, @patchesView]
view.render()
constructor: (options, @levelID) ->
super options
@listenToOnce(@supermodel, 'loaded-all', @onAllLoaded)
@ -93,6 +101,8 @@ module.exports = class EditorLevelView extends View
Backbone.Mediator.publish 'level-loaded', level: @level
@showReadOnly() if me.get('anonymous')
@patchesView = @insertSubView(new PatchesView(@level), @$el.find('.patches-view'))
@listenTo @patchesView, 'accepted-patch', -> setTimeout "location.reload()", 400
@$el.find('#level-watch-button').find('> span').toggleClass('secret') if @level.watching()
onPlayLevel: (e) ->
sendLevel = =>
@ -124,3 +134,8 @@ module.exports = class EditorLevelView extends View
versionHistoryView = new VersionHistoryView level:@level, @levelID
@openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e
toggleWatchLevel: ->
button = @$el.find('#level-watch-button')
@level.watch(button.find('.watch').is(':visible'))
button.find('> span').toggleClass('secret')

View file

@ -30,7 +30,7 @@ module.exports = class LevelSaveView extends SaveVersionModal
context
afterRender: ->
super()
super(false)
changeEls = @$el.find('.changes-stub')
models = if @lastContext.levelNeedsSave then [@level] else []
models = models.concat @lastContext.modifiedComponents

View file

@ -1,6 +1,9 @@
View = require 'views/kinds/CocoView'
template = require 'templates/editor/level/system/edit'
LevelSystem = require 'models/LevelSystem'
VersionHistoryView = require 'views/editor/system/versions_view'
PatchesView = require 'views/editor/patches_view'
SaveVersionModal = require 'views/modal/save_version_modal'
module.exports = class LevelSystemEditView extends View
id: "editor-level-system-edit-view"
@ -10,6 +13,13 @@ module.exports = class LevelSystemEditView extends View
events:
'click #done-editing-system-button': 'endEditing'
'click .nav a': (e) -> $(e.target).tab('show')
'click #system-patches-tab': -> @patchesView.load()
'click #system-code-tab': 'buildCodeEditor'
'click #system-config-schema-tab': 'buildConfigSchemaTreema'
'click #system-settings-tab': 'buildSettingsTreema'
'click #system-history-button': 'showVersionHistory'
'click #patch-system-button': 'startPatchingSystem'
'click #system-watch-button': 'toggleWatchSystem'
constructor: (options) ->
super options
@ -26,6 +36,7 @@ module.exports = class LevelSystemEditView extends View
@buildSettingsTreema()
@buildConfigSchemaTreema()
@buildCodeEditor()
@patchesView = @insertSubView(new PatchesView(@levelSystem), @$el.find('.patches-view'))
buildSettingsTreema: ->
data = _.pick @levelSystem.attributes, (value, key) => key in @editableSettings
@ -68,8 +79,9 @@ module.exports = class LevelSystemEditView extends View
Backbone.Mediator.publish 'level-system-edited', levelSystem: @levelSystem
buildCodeEditor: ->
editorEl = @$el.find '#system-code-editor'
editorEl.text @levelSystem.get('code')
@editor?.destroy()
editorEl = $('<div></div>').text(@levelSystem.get('code')).addClass('inner-editor')
@$el.find('#system-code-editor').empty().append(editorEl)
@editor = ace.edit(editorEl[0])
@editor.setReadOnly(not me.isAdmin())
session = @editor.getSession()
@ -88,6 +100,21 @@ module.exports = class LevelSystemEditView extends View
Backbone.Mediator.publish 'level-system-editing-ended', levelSystem: @levelSystem
null
showVersionHistory: (e) ->
versionHistoryView = new VersionHistoryView {}, @levelSystem.id
@openModalView versionHistoryView
Backbone.Mediator.publish 'level:view-switched', e
startPatchingSystem: (e) ->
@openModalView new SaveVersionModal({model:@levelSystem})
Backbone.Mediator.publish 'level:view-switched', e
toggleWatchSystem: ->
console.log 'toggle watch system?'
button = @$el.find('#system-watch-button')
@levelSystem.watch(button.find('.watch').is(':visible'))
button.find('> span').toggleClass('secret')
destroy: ->
@editor?.destroy()
super()

View file

@ -23,6 +23,7 @@ module.exports = class SystemsTabView extends View
events:
'click #add-system-button': 'addLevelSystem'
'click #create-new-system-button': 'createNewLevelSystem'
'click #create-new-system': 'createNewLevelSystem'
constructor: (options) ->
super options

View file

@ -255,7 +255,7 @@ module.exports = class ThangsTabView extends View
# @thangsTreema.deselectAll()
selectAddThang: (e) =>
return unless $(e.target).closest('.editor-level-thangs-tab-view').length
return unless e? and $(e.target).closest('.editor-level-thangs-tab-view').length
if e then target = $(e.target) else target = @$el.find('.add-thangs-palette') # pretend to click on background if no event
return true if target.attr('id') is 'surface'
target = target.closest('.add-thang-palette-icon')

View file

@ -7,6 +7,7 @@ module.exports = class PatchModal extends ModalView
id: "patch-modal"
template: template
plain: true
modalWidthPercent: 60
events:
'click #withdraw-button': 'withdrawPatch'
@ -30,12 +31,15 @@ module.exports = class PatchModal extends ModalView
c.isPatchCreator = @patch.get('creator') is auth.me.id
c.isPatchRecipient = @targetModel.hasWriteAccess()
c.status = @patch.get 'status'
c.patch = @patch
c
afterRender: ->
return if @originalSource.loading
headModel = @originalSource.clone(false)
headModel.set(@targetModel.attributes)
headModel = null
if @targetModel.hasWriteAccess()
headModel = @originalSource.clone(false)
headModel.set(@targetModel.attributes)
pendingModel = @originalSource.clone(false)
pendingModel.applyDelta(@patch.get('delta'))
@ -48,7 +52,8 @@ module.exports = class PatchModal extends ModalView
acceptPatch: ->
delta = @deltaView.getApplicableDelta()
@targetModel.applyDelta(delta)
@targetModel.addPatchToAcceptOnSave(@patch)
@patch.setStatus('accepted')
@trigger 'accepted-patch'
@hide()
rejectPatch: ->

View file

@ -44,8 +44,11 @@ module.exports = class PatchesView extends CocoView
@$el.find(".#{@status}").addClass 'active'
onStatusButtonsChanged: (e) ->
@loaded = false
@status = $(e.target).val()
@reloadPatches()
reloadPatches: ->
@loaded = false
@initPatches()
@load()
@render()
@ -53,4 +56,9 @@ module.exports = class PatchesView extends CocoView
openPatchModal: (e) ->
patch = _.find @patches.models, {id:$(e.target).data('patch-id')}
modal = new PatchModal(patch, @model)
@openModalView(modal)
@openModalView(modal)
@listenTo modal, 'accepted-patch', -> @trigger 'accepted-patch'
@listenTo modal, 'hide', ->
f = => @reloadPatches()
setTimeout(f, 400)
@stopListening modal

View file

@ -0,0 +1,9 @@
VersionsModalView = require 'views/modal/versions_modal'
module.exports = class SystemVersionsView extends VersionsModalView
id: "editor-system-versions-view"
url: "/db/level.system/"
page: "system"
constructor: (options, @ID) ->
super options, ID, require 'models/LevelSystem'

View file

@ -207,6 +207,7 @@ class CocoView extends Backbone.View
# Modals
toggleModal: (e) ->
return if visibleModal
if $(e.currentTarget).prop('target') is '_blank'
return true
# special handler for opening modals that are dynamically loaded, rather than static in the page. It works (or should work) like Bootstrap's modals, except use coco-modal for the data-toggle value.

View file

@ -45,9 +45,11 @@ module.exports = class ModalView extends CocoView
super($el)
hide: ->
@trigger 'hide'
@$el.removeClass('fade').modal "hide"
onHidden: ->
@trigger 'hidden'
destroy: ->
@hide() unless @hidden

View file

@ -8,6 +8,7 @@ module.exports = class SaveVersionModal extends ModalView
id: 'save-version-modal'
template: template
plain: true
modalWidthPercent: 60
events:
'click #save-version-button': 'onClickSaveButton'
@ -26,15 +27,16 @@ module.exports = class SaveVersionModal extends ModalView
c.hasChanges = @model.hasLocalChanges()
c
afterRender: ->
afterRender: (insertDeltaView=true) ->
super()
@$el.find(if me.get('signedCLA') then '#accept-cla-wrapper' else '#save-version-button').hide()
changeEl = @$el.find('.changes-stub')
try
deltaView = new DeltaView({model:@model})
@insertSubView(deltaView, changeEl)
catch e
console.error "Couldn't create delta view:", e
if insertDeltaView
try
deltaView = new DeltaView({model:@model})
@insertSubView(deltaView, changeEl)
catch e
console.error "Couldn't create delta view:", e
@$el.find('.commit-message input').attr('placeholder', $.i18n.t('general.commit_msg'))
onClickSaveButton: ->
@ -54,6 +56,7 @@ module.exports = class SaveVersionModal extends ModalView
}
errors = patch.validate()
forms.applyErrorsToForm(@$el, errors) if errors
patch.set 'editPath', document.location.pathname
res = patch.save()
return unless res
@enableModalInProgress(@$el)

View file

@ -13,6 +13,8 @@ class VersionsViewCollection extends Backbone.Collection
module.exports = class VersionsModalView extends ModalView
template: template
startsLoading: true
plain: true
modalWidthPercent: 80
# needs to be overwritten by child
id: ""

View file

@ -4,6 +4,8 @@ Grid = require 'gridfs-stream'
errors = require './errors'
log = require 'winston'
Patch = require '../patches/Patch'
User = require '../users/User'
sendwithus = require '../sendwithus'
PROJECT = {original:1, name:1, version:1, description: 1, slug:1, kind: 1}
FETCH_LIMIT = 200
@ -95,7 +97,7 @@ module.exports = class Handler
# this handler should be overwritten by subclasses
if @modelClass.schema.is_patchable
return @getPatchesFor(req, res, args[0]) if req.route.method is 'get' and args[1] is 'patches'
return @setListening(req, res, args[0]) if req.route.method is 'put' and args[1] is 'listen'
return @setWatching(req, res, args[0]) if req.route.method is 'put' and args[1] is 'watch'
return @sendNotFoundError(res)
getPatchesFor: (req, res, id) ->
@ -105,16 +107,16 @@ module.exports = class Handler
patches = (patch.toObject() for patch in patches)
@sendSuccess(res, patches)
setListening: (req, res, id) ->
setWatching: (req, res, id) ->
@getDocumentForIdOrSlug id, (err, document) =>
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, document, 'get')
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless document?
listeners = document.get('listeners') or []
watchers = document.get('watchers') or []
me = req.user.get('_id')
listeners = (l for l in listeners when not l.equals(me))
listeners.push me if req.body.on
document.set 'listeners', listeners
watchers = (l for l in watchers when not l.equals(me))
watchers.push me if req.body.on and req.body.on isnt 'false'
document.set 'watchers', watchers
document.save (err, document) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document))
@ -233,6 +235,9 @@ module.exports = class Handler
return @sendBadInputError(res, err.errors) if err?.valid is false
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document))
@onPostSuccess(req, document)
onPostSuccess: (req, doc) ->
###
TODO: think about pulling some common stuff out of postFirstVersion/postNewVersion
@ -248,7 +253,6 @@ module.exports = class Handler
document.set('original', document._id)
document.set('creator', req.user._id)
@saveChangesToDocument req, document, (err) =>
console.log 'saved new version', document.toObject()
return @sendBadInputError(res, err.errors) if err?.valid is false
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document))
@ -291,6 +295,7 @@ module.exports = class Handler
newDocument.save (err) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, newDocument))
@notifyWatchersOfChange(req.user, newDocument, req.body.editPath) if @modelClass.schema.is_patchable
if major?
parentDocument.makeNewMinorVersion(updatedObject, major, done)
@ -298,8 +303,31 @@ module.exports = class Handler
else
parentDocument.makeNewMajorVersion(updatedObject, done)
notifyWatchersOfChange: (editor, changedDocument, editPath) ->
watchers = changedDocument.get('watchers') or []
watchers = (w for w in watchers when not w.equals(editor.get('_id')))
return unless watchers.length
User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) =>
for watcher in watchers
@notifyWatcherOfChange editor, watcher, changedDocument, editPath
notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) ->
context =
email_id: sendwithus.templates.change_made_notify_watcher
recipient:
address: watcher.get('email')
name: watcher.get('name')
email_data:
doc_name: changedDocument.get('name') or '???'
submitter_name: editor.get('name') or '???'
doc_link: if editPath then "http://codecombat.com#{editPath}" else null
commit_message: changedDocument.get('commitMessage')
sendwithus.api.send context, (err, result) ->
makeNewInstance: (req) ->
new @modelClass({})
model = new @modelClass({})
model.set 'watchers', [req.user.get('_id')] if @modelClass.schema.is_patchable
model
validateDocumentInput: (input) ->
tv4 = require('tv4').tv4

View file

@ -39,8 +39,10 @@ PatchSchema.pre 'save', (next) ->
target.original = targetID
patches = document.get('patches') or []
patches = _.clone patches
patches.push @_id
document.set 'patches', patches
document.set 'patches', patches, {strict: false}
@targetLoaded = document
document.save (err) -> next(err)
module.exports = mongoose.model('patch', PatchSchema)

View file

@ -1,8 +1,11 @@
Patch = require('./Patch')
User = require '../users/User'
Handler = require('../commons/Handler')
schema = require '../../app/schemas/models/patch'
{handlers} = require '../commons/mapping'
mongoose = require('mongoose')
log = require 'winston'
sendwithus = require '../sendwithus'
PatchHandler = class PatchHandler extends Handler
modelClass: Patch
@ -50,6 +53,29 @@ PatchHandler = class PatchHandler extends Handler
patch.update {$set:{status:newStatus}}, {}, ->
target.update {$pull:{patches:patch.get('_id')}}, {}, ->
@sendSuccess(res, null)
onPostSuccess: (req, doc) ->
log.error "Error sending patch created: could not find the loaded target on the patch object." unless doc.targetLoaded
return unless doc.targetLoaded
watchers = doc.targetLoaded.get('watchers') or []
watchers = (w for w in watchers when not w.equals(editor.get('_id')))
return unless watchers?.length
User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) =>
for watcher in watchers
@sendPatchCreatedEmail req.user, watcher, doc, doc.targetLoaded, req.body.editPath
sendPatchCreatedEmail: (patchCreator, watcher, patch, target, editPath) ->
# return if watcher._id is patchCreator._id
context =
email_id: sendwithus.templates.patch_created
recipient:
address: watcher.get('email')
name: watcher.get('name')
email_data:
doc_name: target.get('name') or '???'
submitter_name: patchCreator.get('name') or '???'
doc_link: "http://codecombat.com#{editPath}"
commit_message: patch.get('commitMessage')
sendwithus.api.send context, (err, result) ->
module.exports = new PatchHandler()

View file

@ -14,3 +14,5 @@ if config.unittest
module.exports.templates =
welcome_email: 'utnGaBHuSU4Hmsi7qrAypU'
ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4'
patch_created: 'tem_xhxuNosLALsizTNojBjNcL'
change_made_notify_watcher: 'tem_7KVkfmv9SZETb25dtHbUtG'

View file

@ -64,13 +64,13 @@ GLOBAL.unittest = {}
unittest.users = unittest.users or {}
unittest.getNormalJoe = (done, force) ->
unittest.getUser('normal@jo.com', 'food', done, force)
unittest.getUser('Joe', 'normal@jo.com', 'food', done, force)
unittest.getOtherSam = (done, force) ->
unittest.getUser('other@sam.com', 'beer', done, force)
unittest.getUser('Sam', 'other@sam.com', 'beer', done, force)
unittest.getAdmin = (done, force) ->
unittest.getUser('admin@afc.com', '80yqxpb38j', done, force)
unittest.getUser('Admin', 'admin@afc.com', '80yqxpb38j', done, force)
unittest.getUser = (email, password, done, force) ->
unittest.getUser = (name, email, password, done, force) ->
# Creates the user if it doesn't already exist.
return done(unittest.users[email]) if unittest.users[email] and not force
@ -81,6 +81,7 @@ unittest.getUser = (email, password, done, force) ->
throw err if err
User.findOne({email:email}).exec((err, user) ->
user.set('permissions', if password is '80yqxpb38j' then [ 'admin' ] else [])
user.set('name', name)
user.save (err) ->
wrapUpGetUser(email, user, done)
)
@ -88,7 +89,6 @@ unittest.getUser = (email, password, done, force) ->
form = req.form()
form.append('email', email)
form.append('password', password)
wrapUpGetUser = (email, user, done) ->
unittest.users[email] = user

View file

@ -16,13 +16,14 @@ describe '/db/patch', ->
patch =
commitMessage: 'Accept this patch!'
delta: {name:['test']}
editPath: '/who/knows/yes'
target:
id:null
collection: 'article'
it 'creates an Article to patch', (done) ->
loginAdmin ->
request.post {uri:articleURL, json:patch}, (err, res, body) ->
request.post {uri:articleURL, json:article}, (err, res, body) ->
articles[0] = body
patch.target.id = articles[0]._id
done()
@ -51,29 +52,29 @@ describe '/db/patch', ->
body = JSON.parse(body)
expect(res.statusCode).toBe(200)
expect(body.length).toBe(1)
done()
done()
it 'allows you to set yourself as listening', (done) ->
listeningURL = getURL("/db/article/#{articles[0]._id}/listen")
request.put {uri: listeningURL, json: {on:true}}, (err, res, body) ->
expect(body.listeners[0]).toBeDefined()
it 'allows you to set yourself as watching', (done) ->
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: watchingURL, json: {on:true}}, (err, res, body) ->
expect(body.watchers[1]).toBeDefined()
done()
it 'added the listener to the target document', (done) ->
it 'added the watcher to the target document', (done) ->
Article.findOne({}).exec (err, article) ->
expect(article.toObject().listeners[0]).toBeDefined()
expect(article.toObject().watchers[1]).toBeDefined()
done()
it 'does not add duplicate listeners', (done) ->
listeningURL = getURL("/db/article/#{articles[0]._id}/listen")
request.put {uri: listeningURL, json: {on:true}}, (err, res, body) ->
expect(body.listeners.length).toBe(1)
it 'does not add duplicate watchers', (done) ->
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: watchingURL, json: {on:true}}, (err, res, body) ->
expect(body.watchers.length).toBe(2)
done()
it 'allows removing yourself', (done) ->
listeningURL = getURL("/db/article/#{articles[0]._id}/listen")
request.put {uri: listeningURL, json: {on:false}}, (err, res, body) ->
expect(body.listeners.length).toBe(0)
watchingURL = getURL("/db/article/#{articles[0]._id}/watch")
request.put {uri: watchingURL, json: {on:false}}, (err, res, body) ->
expect(body.watchers.length).toBe(1)
done()
it 'allows the submitter to withdraw the pull request', (done) ->

View file

@ -112,7 +112,7 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
unittest.getNormalJoe (joe) ->
req = request.put getURL(urlUser), (err, res) ->
expect(res.statusCode).toBe(200)
unittest.getUser('New@email.com', 'null', (joe) ->
unittest.getUser('Wilhelm', 'New@email.com', 'null', (joe) ->
expect(joe.get('name')).toBe('Wilhelm')
expect(joe.get('emailLower')).toBe('new@email.com')
expect(joe.get('email')).toBe('New@email.com')