Rework left sidebar

* Move Buttons from left sidebar to JSTree
* Use light style for collapse sidebar buttons
This commit is contained in:
Sebastian Serth
2021-05-14 16:00:54 +02:00
parent 6f084afe1c
commit f32661ad78
12 changed files with 80 additions and 57 deletions

View File

@ -198,7 +198,7 @@ var CodeOceanEditor = {
}, },
hideSpinner: function () { hideSpinner: function () {
$('button i.fa').show(); $('button i.fa, button i.far, button i.fas').show();
$('button i.fa-spin').hide(); $('button i.fa-spin').hide();
}, },
@ -212,7 +212,7 @@ var CodeOceanEditor = {
resizeParentOfAceEditor: function (element) { resizeParentOfAceEditor: function (element) {
// calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins // calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins
var windowHeight = window.innerHeight - $(element).offset().top - $('#autosave-label').height() - 5; var windowHeight = window.innerHeight - $(element).offset().top - $('#statusbar').height() - 5;
$(element).parent().height(windowHeight); $(element).parent().height(windowHeight);
}, },
@ -245,7 +245,7 @@ var CodeOceanEditor = {
document.insertLines(0, content.text().split(/\n/)); document.insertLines(0, content.text().split(/\n/));
// remove last (empty) that is there by default line // remove last (empty) that is there by default line
document.removeLines(document.getLength() - 1, document.getLength() - 1); document.removeLines(document.getLength() - 1, document.getLength() - 1);
editor.setReadOnly($(element).data('read-only') !== undefined); editor.setReadOnly($(element).parent().data('read-only') !== undefined);
if (editor.getReadOnly()) { if (editor.getReadOnly()) {
editor.setHighlightActiveLine(false); editor.setHighlightActiveLine(false);
editor.setHighlightGutterLine(false); editor.setHighlightGutterLine(false);
@ -341,11 +341,9 @@ var CodeOceanEditor = {
initializeFileTreeButtons: function () { initializeFileTreeButtons: function () {
$('#create-file').on('click', this.showFileDialog.bind(this)); $('#create-file').on('click', this.showFileDialog.bind(this));
$('#create-file-collapsed').on('click', this.showFileDialog.bind(this));
$('#destroy-file').on('click', this.confirmDestroy.bind(this)); $('#destroy-file').on('click', this.confirmDestroy.bind(this));
$('#destroy-file-collapsed').on('click', this.confirmDestroy.bind(this)); $('#destroy-file-collapsed').on('click', this.confirmDestroy.bind(this));
$('#download').on('click', this.downloadCode.bind(this)); $('#download').on('click', this.downloadCode.bind(this));
$('#download-collapsed').on('click', this.downloadCode.bind(this));
$('#request-for-comments').on('click', this.requestComments.bind(this)); $('#request-for-comments').on('click', this.requestComments.bind(this));
}, },
@ -580,7 +578,7 @@ var CodeOceanEditor = {
toggleButtonStates: function () { toggleButtonStates: function () {
$('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file'); $('#destroy-file').prop('disabled', this.active_frame.data('role') !== 'user_defined_file');
$('#start-over-active-file').prop('disabled', this.active_frame.data('role') === 'user_defined_file'); $('#start-over-active-file').prop('disabled', this.active_frame.data('role') === 'user_defined_file' || this.active_frame.data('read-only') !== undefined);
$('#dummy').toggle(!this.fileActionsAvailable()); $('#dummy').toggle(!this.fileActionsAvailable());
$('#render').toggle(this.isActiveFileRenderable()); $('#render').toggle(this.isActiveFileRenderable());
$('#run').toggle(this.isActiveFileRunnable() && !this.running); $('#run').toggle(this.isActiveFileRunnable() && !this.running);
@ -654,7 +652,7 @@ var CodeOceanEditor = {
}, },
showSpinner: function (initiator) { showSpinner: function (initiator) {
$(initiator).find('i.fa').hide(); $(initiator).find('i.fa, i.far, i.fas').hide();
$(initiator).find('i.fa-spin').show(); $(initiator).find('i.fa-spin').show();
}, },

View File

@ -3,7 +3,7 @@ CodeOceanEditorSubmissions = {
AUTOSAVE_INTERVAL: 15 * 1000, AUTOSAVE_INTERVAL: 15 * 1000,
autosaveTimer: null, autosaveTimer: null,
autosaveLabel: "#autosave-label span", autosaveLabel: "#statusbar span",
/** /**
* Submission-Creation * Submission-Creation

View File

@ -12,7 +12,7 @@ h1, h2, h3, h4, h5, h6 {
color: rgba(70, 70, 70, 1); color: rgba(70, 70, 70, 1);
} }
i.fa { i.fa, i.far, i.fas {
margin-right: 0.5em; margin-right: 0.5em;
} }

View File

@ -85,10 +85,10 @@ button i.fa-spin {
display: none; display: none;
} }
#autosave-label{ #statusbar{
visibility: hidden; visibility: hidden;
margin-top: .2em;
height: 1.6em; height: 1.6em;
text-align: right;
color: #777; color: #777;
font-size: 0.8em; font-size: 0.8em;
} }

View File

@ -18,7 +18,7 @@ module CodeOcean
@file.content = content @file.content = content
end end
authorize! authorize!
create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) }) create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise) })
end end
def create_and_respond(options = {}) def create_and_respond(options = {})

View File

@ -24,13 +24,29 @@
= render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) unless @embed_options[:disable_score] = render('editor_button', data: {:'data-placement' => 'top', :'data-toggle' => 'tooltip', :'data-container' => 'body'}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) unless @embed_options[:disable_score]
- unless hide_rfc_button - unless hide_rfc_button
= render('editor_button', icon: 'fa fa-comment', id: 'requestComments', label: t('exercises.editor.requestComments'), title: t('exercises.editor.requestCommentsTooltip')) = render('editor_button', icon: 'fa fa-comment', id: 'requestComments', label: t('exercises.editor.requestComments'), title: t('exercises.editor.requestCommentsTooltip'))
- @files.each do |file| - @files.each do |file|
- file.read_only = true if @embed_options[:read_only] - file.read_only = true if @embed_options[:read_only]
= render('editor_frame', exercise: exercise, file: file) = render('editor_frame', exercise: exercise, file: file)
#autosave-label
= t('exercises.editor.lastsaved') #statusbar.d-flex.justify-content-between
span div
button style="display:none" id="autosave" - if !@embed_options[:disable_download] && @exercise.hide_file_tree?
button#download.p-0.border-0.btn-link.visible.bg-white
i.fas.fa-arrow-down
= t('exercises.editor.download')
div
= t('exercises.editor.lastsaved')
span
button style="display:none" id="autosave"
= " | "
button#start-over-active-file.p-0.border-0.btn-link.bg-white data-message-confirm=t('exercises.editor.confirm_start_over_active_file') data-url=reload_exercise_path(@exercise)
i.fa.fa-history
= t('exercises.editor.start_over_active_file')
- unless @embed_options[:disable_run] && @embed_options[:disable_score] - unless @embed_options[:disable_run] && @embed_options[:disable_score]
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )

View File

@ -1,4 +1,4 @@
button.btn class=local_assigns.fetch(:classes, 'btn-primary') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button' button.btn class=local_assigns.fetch(:classes, 'btn-primary') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button'
i.fa.fa-circle-o-notch.fa-spin i.fa.fa-circle-o-notch.fa-spin class=(label.blank? ? "m-0" : '')
i class=(label.present? ? icon : "#{icon} m-0") i class=(label.present? ? icon : "#{icon} m-0")
= label = label

View File

@ -1,44 +1,42 @@
div id='sidebar-collapsed' class=(@exercise.hide_file_tree && @tips.blank? ? '' : 'd-none') div id='sidebar-collapsed' class=(@exercise.hide_file_tree && @tips.blank? ? '' : 'd-none')
= render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar')) = render('editor_button', classes: 'btn-block btn-outline-dark btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar'))
- if @exercise.allow_file_creation and not @exercise.hide_file_tree?
= render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-cause' => 'file', :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus', id: 'create-file-collapsed', label:'', title: t('exercises.editor.create_file'))
- unless @embed_options[:disable_hints] or @tips.blank? - unless @embed_options[:disable_hints] or @tips.blank?
= render('editor_button', classes: 'btn-block btn-secondary btn mb-4', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-lightbulb', id: 'tips-collapsed', label:'', title: t('exercises.form.tips')) = render('editor_button', classes: 'btn-block btn-secondary btn mb-4', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-lightbulb', id: 'tips-collapsed', label:'', title: t('exercises.form.tips'))
- unless @embed_options[:disable_download]
= render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download'))
= render('editor_button', classes: "btn-block btn-outline-warning btn enforce-top-margin #{@exercise.hide_file_tree || files.count < 2 && !@exercise.allow_file_creation ? 'd-none' : ''}", data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over_active_file'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-active-file-collapsed', label: '', title: t('exercises.editor.start_over_active_file'))
//- if !@course_token.blank? //- if !@course_token.blank?
= render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum'))
= render('editor_button', classes: 'btn-block btn-outline-danger btn enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over'))
div.h-100.col-sm-12.enforce-bottom-margin id='sidebar-uncollapsed' class=(@exercise.hide_file_tree && @tips.blank? ? 'd-none' : '') div.h-100.col-sm-12.enforce-bottom-margin id='sidebar-uncollapsed' class=(@exercise.hide_file_tree && @tips.blank? ? 'd-none' : '')
.position-absolute.d-flex.mb-1.w-100 style="overflow: auto; left: 0; top: 0; height: 100%;" .position-absolute.d-flex.mb-1.w-100 style="overflow: auto; left: 0; top: 0; height: 100%;"
.w-100 .w-100
= render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) = render('editor_button', classes: 'btn-block btn-outline-dark btn', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar'))
div class=(@exercise.hide_file_tree ? 'd-none' : '') - unless @exercise.hide_file_tree
hr div
hr
#files data-entries=FileTree.new(files).to_js_tree .card.border-secondary
.card-header.d-flex.justify-content-between.align-items-center.px-0.py-1
.px-2 = I18n.t('exercises.editor_file_tree.file_root')
div
- if @exercise.allow_file_creation
= render('editor_button', classes: 'btn-default btn-sm', data: {:'data-toggle' => 'tooltip', :'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: '', title: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-default btn-sm', data: {:'data-toggle' => 'tooltip', :'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy') }, icon: 'far fa-trash-alt', id: 'destroy-file', label: '', title: t('exercises.editor.destroy_file'))
- unless @embed_options[:disable_download]
= render('editor_button', classes: 'btn-default btn-sm', data: {:'data-toggle' => 'tooltip'}, icon: 'fas fa-arrow-down', id: 'download', label:'', title: t('exercises.editor.download'))
= render('editor_button', classes: 'btn-default btn-sm', data: {:'data-toggle' => 'tooltip', :'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: '', title: t('exercises.editor.start_over'))
hr .card-body.pt-0.pr-0.pl-1.pb-1
- if @exercise.allow_file_creation and not @exercise.hide_file_tree? #files data-entries=FileTree.new(files).to_js_tree
= render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-block btn-warning btn', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file')) hr
- unless @embed_options[:disable_hints] or @tips.blank? - unless @embed_options[:disable_hints] or @tips.blank?
= render(partial: 'tips_content') = render(partial: 'tips_content')
.mb-4 .mb-4
- unless @embed_options[:disable_download]
= render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))
= render('editor_button', classes: "btn-block btn-outline-warning btn #{@exercise.hide_file_tree || files.count < 2 && !@exercise.allow_file_creation ? 'd-none' : ''}", data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over_active_file'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over-active-file', label: t('exercises.editor.start_over_active_file'))
= render('editor_button', classes: 'btn-block btn-outline-danger btn', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
//- if !@course_token.blank? //- if !@course_token.blank?
.input-group.enforce-top-margin .input-group.enforce-top-margin
.enforce-right-margin .enforce-right-margin
@ -48,4 +46,4 @@ div.h-100.col-sm-12.enforce-bottom-margin id='sidebar-uncollapsed' class=(@exerc
i.fa.fa-search i.fa.fa-search
- if @exercise.allow_file_creation? - if @exercise.allow_file_creation?
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))

View File

@ -1,4 +1,4 @@
.frame data-executable=file.file_type.executable? data-filename=file.name_with_extension data-renderable=file.file_type.renderable? data-role=file.role data-binary=file.file_type.binary? data-context-type=file.context_type .frame data-executable=file.file_type.executable? data-filename=file.name_with_extension data-renderable=file.file_type.renderable? data-role=file.role data-binary=file.file_type.binary? data-context-type=file.context_type data-read-only=file.read_only
- if file.file_type.binary? - if file.file_type.binary?
.binary-file data-file-id=file.ancestor_id .binary-file data-file-id=file.ancestor_id
- if file.file_type.renderable? - if file.file_type.renderable?
@ -12,4 +12,4 @@
= link_to(file.native_file.file.filename, file.native_file.url) = link_to(file.native_file.file.filename, file.native_file.url)
- else - else
.editor-content.d-none data-file-id=file.ancestor_id = file.content .editor-content.d-none data-file-id=file.ancestor_id = file.content
.editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only data-allow-auto-completion=exercise.allow_auto_completion.to_s data-id=file.id .editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-allow-auto-completion=exercise.allow_auto_completion.to_s data-id=file.id

View File

@ -1,8 +1,8 @@
div id='output_sidebar_collapsed' div id='output_sidebar_collapsed'
= render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') = render('editor_button', classes: 'btn-block btn-outline-dark btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '')
div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output_yet') div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output_yet')
.row .row
= render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar')) = render('editor_button', classes: 'btn-block btn-outline-dark btn', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar'))
div.position-absolute.d-flex.mb-1.w-100 style="overflow: auto; left: 0; bottom: 0; height: calc(100% - 3rem);" div.position-absolute.d-flex.mb-1.w-100 style="overflow: auto; left: 0; bottom: 0; height: calc(100% - 3rem);"
div.w-100 div.w-100

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class FileTree < Tree::TreeNode class FileTree
def file_icon(file) def file_icon(file)
if file.file_type.audio? if file.file_type.audio?
'fa fa-file-audio-o' 'fa fa-file-audio-o'
@ -26,9 +26,11 @@ class FileTree < Tree::TreeNode
private :folder_icon private :folder_icon
def initialize(files = []) def initialize(files = [])
super(root_label) # Our tree needs a root node, but we won't display it.
@root = Tree::TreeNode.new('ROOT')
files.uniq(&:name_with_extension).each do |file| files.uniq(&:name_with_extension).each do |file|
parent = self parent = @root
(file.path || '').split('/').each do |segment| (file.path || '').split('/').each do |segment|
node = parent.children.detect {|child| child.name == segment } || parent.add(Tree::TreeNode.new(segment)) node = parent.children.detect {|child| child.name == segment } || parent.add(Tree::TreeNode.new(segment))
parent = node parent = node
@ -60,15 +62,10 @@ class FileTree < Tree::TreeNode
end end
private :node_icon private :node_icon
def root_label
I18n.t('exercises.editor_file_tree.file_root')
end
private :root_label
def to_js_tree def to_js_tree
{ {
core: { core: {
data: map_to_js_tree(self), data: @root.children.map {|child| map_to_js_tree(child) },
}, },
}.to_json }.to_json
end end

View File

@ -81,16 +81,16 @@ describe FileTree do
it 'creates a root node' do it 'creates a root node' do
# Instead of checking #initialize on the parent, we validate #set_as_root! # Instead of checking #initialize on the parent, we validate #set_as_root!
expect(file_tree).to receive(:set_as_root!).and_call_original expect(Tree::TreeNode).to receive(:new).and_call_original.at_least(:once)
file_tree.send(:initialize) file_tree.send(:initialize)
end end
it 'creates tree nodes for every file' do it 'creates tree nodes for every file' do
expect(file_tree.select(&:content).map(&:content)).to eq(files) expect(file_tree.instance_variable_get(:@root).select(&:content).map(&:content)).to eq(files)
end end
it 'creates tree nodes for intermediary path segments' do it 'creates tree nodes for intermediary path segments' do
expect(file_tree.reject(&:content).reject(&:is_root?).map(&:name)).to eq(files.first.path.split('/')) expect(file_tree.instance_variable_get(:@root).reject(&:content).reject(&:is_root?).map(&:name)).to eq(files.first.path.split('/'))
end end
end end
@ -179,8 +179,22 @@ describe FileTree do
expect(js_tree).to be_a(String) expect(js_tree).to be_a(String)
end end
it 'produces the required JSON format' do context 'without any file' do
expect(JSON.parse(js_tree).deep_symbolize_keys).to eq(core: {data: file_tree.send(:map_to_js_tree, file_tree)}) it 'produces the required JSON format' do
expect(JSON.parse(js_tree).deep_symbolize_keys).to eq(core: {data: []})
end
end
context 'with files' do
let(:files) { FactoryBot.build_list(:file, 10, context: nil, path: 'foo/bar/baz') }
let(:file_tree) { described_class.new(files) }
let(:js_tree) { file_tree.to_js_tree }
it 'produces the required JSON format with a file' do
# We ignore the root node and only use the children here
child_tree = file_tree.send(:map_to_js_tree, file_tree.instance_variable_get(:@root).children.first)
expect(JSON.parse(js_tree).deep_symbolize_keys).to eq(core: {data: [child_tree]})
end
end end
end end
end end