From 2ea4fd5b599525bae6629a5acd2feac75e08d5f2 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 6 Aug 2024 10:36:35 +0200 Subject: [PATCH 01/28] DIY first version --- .../javascripts/components/input_table.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 app/assets/javascripts/components/input_table.ts diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts new file mode 100644 index 0000000000..7bf76e79d5 --- /dev/null +++ b/app/assets/javascripts/components/input_table.ts @@ -0,0 +1,102 @@ +/** + * This component creates an excel-like table with input fields. + * + * It recieves data in the format of a 2D array, and renders the table accordingly. + * It always shows an empty row at the end, to allow for adding new rows. + * + * It als takes an optional 'headers' property, which is an array of strings to be used as headers. + * + * + */ + +import { customElement, property } from "lit/decorators.js"; +import { html, LitElement, TemplateResult } from "lit"; + +type CellData = string | number | boolean; + +@customElement("d-input-table") +export class DInputTable extends LitElement { + @property({ type: Array }) + data: Record[] = []; + @property({ type: Object }) + headers: Record = {}; + @property({ type: Array }) + columns: string[] = []; + @property({ type: Array }) + required: string[] = []; + + @property({ type: Array, state: true }) + private errors: Record> = {}; + + updateValue(e: Event, row: Record, col: string, index: number): void { + const target = e.target as HTMLInputElement; + const value = target.value; + const newRow = { ...row, [col]: value }; + if (this.checkErrors(newRow, index)) { + return; + } + const event = new CustomEvent("update", { + detail: newRow + }); + this.dispatchEvent(event); + } + + checkErrors(row: Record, index: number): boolean { + let hasError = false; + this.errors[index] = {}; + this.required.forEach(col => { + if (!row[col]) { + if (!this.errors[index]) { + this.errors[index] = {}; + } + this.errors[index][col] = "This field is required"; + hasError = true; + } else { + delete this.errors[index][col]; + } + }); + if (!hasError) { + delete this.errors[index]; + } + + return hasError; + } + + render(): TemplateResult { + return html` + + + + ${this.columns.map(col => html``)} + + + + ${this.data.map((row, index) => html` + + ${this.columns.map(col => html` + + `)} + + `)} + + ${this.columns.map(col => html` + + `)} + + +
${col}
+ this.updateValue(e, row, col, index)} + class="${this.errors[index] && this.errors[index][col] ? "error" : ""}" + /> +
+ this.updateValue(e, {}, col, this.data.length)} + class="${this.errors[this.data.length] && this.errors[this.data.length][col] ? "error" : ""}" + /> +
+ `; + } +} From 935ce060d112156ae9ea313b0a79c97dd6c457a0 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 6 Aug 2024 14:47:08 +0200 Subject: [PATCH 02/28] Use jspreadsheet --- .../javascripts/components/input_table.ts | 114 ++++-------------- app/assets/stylesheets/base.css.scss | 4 + app/javascript/packs/score_item.js | 1 + app/views/score_items/_exercise.html.erb | 1 + package.json | 3 +- yarn.lock | 18 +++ 6 files changed, 51 insertions(+), 90 deletions(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index 7bf76e79d5..16ad93c61e 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -1,102 +1,38 @@ -/** - * This component creates an excel-like table with input fields. - * - * It recieves data in the format of a 2D array, and renders the table accordingly. - * It always shows an empty row at the end, to allow for adding new rows. - * - * It als takes an optional 'headers' property, which is an array of strings to be used as headers. - * - * - */ - import { customElement, property } from "lit/decorators.js"; -import { html, LitElement, TemplateResult } from "lit"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import jspreadsheet, { JspreadsheetInstance, Column } from "jspreadsheet-ce"; +import { createRef, ref, Ref } from "lit/directives/ref.js"; +import { DodonaElement } from "components/meta/dodona_element"; type CellData = string | number | boolean; + @customElement("d-input-table") -export class DInputTable extends LitElement { - @property({ type: Array }) - data: Record[] = []; - @property({ type: Object }) - headers: Record = {}; +export class DInputTable extends DodonaElement { @property({ type: Array }) - columns: string[] = []; + data: CellData[][] = []; @property({ type: Array }) - required: string[] = []; - - @property({ type: Array, state: true }) - private errors: Record> = {}; - - updateValue(e: Event, row: Record, col: string, index: number): void { - const target = e.target as HTMLInputElement; - const value = target.value; - const newRow = { ...row, [col]: value }; - if (this.checkErrors(newRow, index)) { - return; - } - const event = new CustomEvent("update", { - detail: newRow + columns: Column[] = []; + + tableRef: Ref = createRef(); + + protected firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + const table: JspreadsheetInstance = jspreadsheet(this.tableRef.value, { + root: this, + data: [ + ["Test", "Test", 1, true], + ], + columns: [ + { type: "text", title: "Naam", width: 120 }, + { type: "text", title: "Beschrijving", width: 120 }, + { type: "numeric", title: "Maximum", width: 120 }, + { type: "checkbox", title: "Zichtbaar", width: 120 } + ] }); - this.dispatchEvent(event); - } - - checkErrors(row: Record, index: number): boolean { - let hasError = false; - this.errors[index] = {}; - this.required.forEach(col => { - if (!row[col]) { - if (!this.errors[index]) { - this.errors[index] = {}; - } - this.errors[index][col] = "This field is required"; - hasError = true; - } else { - delete this.errors[index][col]; - } - }); - if (!hasError) { - delete this.errors[index]; - } - - return hasError; } render(): TemplateResult { - return html` - - - - ${this.columns.map(col => html``)} - - - - ${this.data.map((row, index) => html` - - ${this.columns.map(col => html` - - `)} - - `)} - - ${this.columns.map(col => html` - - `)} - - -
${col}
- this.updateValue(e, row, col, index)} - class="${this.errors[index] && this.errors[index][col] ? "error" : ""}" - /> -
- this.updateValue(e, {}, col, this.data.length)} - class="${this.errors[this.data.length] && this.errors[this.data.length][col] ? "error" : ""}" - /> -
- `; + return html`
`; } } diff --git a/app/assets/stylesheets/base.css.scss b/app/assets/stylesheets/base.css.scss index 796425716e..6b61dc6545 100644 --- a/app/assets/stylesheets/base.css.scss +++ b/app/assets/stylesheets/base.css.scss @@ -64,6 +64,10 @@ @import "../../../node_modules/bootstrap/scss/offcanvas"; @import "../../../node_modules/bootstrap/scss/utilities/api"; +// jspreadsheet styles +@import "../../../node_modules/jspreadsheet-ce/dist/jspreadsheet"; +@import "../../../node_modules/jsuites/dist/jsuites"; + :root { --scrollbar-width: 20px; } diff --git a/app/javascript/packs/score_item.js b/app/javascript/packs/score_item.js index 79aeb544fc..908ce7c390 100644 --- a/app/javascript/packs/score_item.js +++ b/app/javascript/packs/score_item.js @@ -1,3 +1,4 @@ import { initVisibilityCheckboxes } from "score_item.ts"; +import "components/input_table.ts"; window.dodona.initVisibilityCheckboxes = initVisibilityCheckboxes; diff --git a/app/views/score_items/_exercise.html.erb b/app/views/score_items/_exercise.html.erb index 4d88cf0b35..25d23d45f6 100644 --- a/app/views/score_items/_exercise.html.erb +++ b/app/views/score_items/_exercise.html.erb @@ -104,6 +104,7 @@ <%= render 'score_items/copy', evaluation_exercise: evaluation_exercise, evaluation: @evaluation %> + diff --git a/config/locales/js/en.yml b/config/locales/js/en.yml index 7a3cece355..f6a0a15b1f 100644 --- a/config/locales/js/en.yml +++ b/config/locales/js/en.yml @@ -317,3 +317,4 @@ en: deleted_warning: You have deleted one or more score items. This will also delete all scores for these items. validation_warning: All score items must have a name and a maximum score, and the maximum score must be greater than 0. save: Save + cancel: Cancel diff --git a/config/locales/js/nl.yml b/config/locales/js/nl.yml index bacc3346ed..ee1348306b 100644 --- a/config/locales/js/nl.yml +++ b/config/locales/js/nl.yml @@ -314,6 +314,7 @@ nl: visible_help: Maak het scoreonderdeel zichtbaar voor studenten eens de evaluatie vrijgegeven is. maximum_help: De maximumscore voor dit scoreonderdeel. Dit moet een positief getal zijn en gaat in stappen van 0.25. save: Opslaan + cancel: Annuleren modified_warning: "Je hebt de maximumscore van een of meerdere scoreonderdelen aangepast. Alle afgewerkte evaluaties met dit scoreonderdeel zullen terug als onafgewerkt gemarkeerd worden." deleted_warning: "Je hebt een of meerdere scoreonderdelen verwijderd. Dit zal ook de bijhorende scores van de studenten verwijderen." validation_warning: "Alle scoreonderdelen moeten een naam en een maximumscore hebben. De maximumscore moet een positief getal zijn." From 2a0a4a8ae9bb5d9036b5dd6fbd340c26d5c90a62 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 8 Aug 2024 12:02:01 +0200 Subject: [PATCH 17/28] Remove checkboxes --- app/assets/javascripts/score_item.ts | 12 ------------ app/views/score_items/_exercise.html.erb | 17 +++++++++++------ config/locales/views/score_items/en.yml | 1 - config/locales/views/score_items/nl.yml | 1 - 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/score_item.ts b/app/assets/javascripts/score_item.ts index cbf612f036..0ec6f83337 100644 --- a/app/assets/javascripts/score_item.ts +++ b/app/assets/javascripts/score_item.ts @@ -45,20 +45,8 @@ function initTotalVisibilityCheckboxes(element: HTMLElement): void { }); } -function initItemVisibilityCheckboxes(element: HTMLElement): void { - commonCheckboxInit(element, ".visibility-checkbox", checked => { - return { - - score_item: { - visible: checked - } - }; - }); -} - export function initVisibilityCheckboxes(element: HTMLElement): void { initTotalVisibilityCheckboxes(element); - initItemVisibilityCheckboxes(element); } export function initEditButton(element: HTMLElement): void { diff --git a/app/views/score_items/_exercise.html.erb b/app/views/score_items/_exercise.html.erb index b5f5726f7a..07d0a15040 100644 --- a/app/views/score_items/_exercise.html.erb +++ b/app/views/score_items/_exercise.html.erb @@ -31,8 +31,10 @@ <%= markdown score_item.description %> <%= format_score score_item.maximum %> - <%= form_for [@evaluation, score_item], namespace: "#{evaluation_exercise.id}_#{score_item.id}", method: :patch, remote: :true do |f| %> - <%= f.check_box :visible, class: "form-check-input visibility-checkbox", title: t(".visibility") %> + <% if score_item.visible %> + + <% else %> + <% end %> @@ -45,10 +47,13 @@ <%= form_for evaluation_exercise, namespace: evaluation_exercise.id, method: :patch, remote: :true do |f| %> - <%= f.check_box :visible_score, - class: "form-check-input total-visibility-checkbox", - disabled: maximum_score.nil?, - title: t(".total_visibility") %> +
+ <%= f.check_box :visible_score, + class: "form-check-input total-visibility-checkbox", + disabled: maximum_score.nil?, + title: t(".total_visibility"), + 'data-bs-toggle': "tooltip" %> +
<% end %> diff --git a/config/locales/views/score_items/en.yml b/config/locales/views/score_items/en.yml index af231fc249..52797e01b4 100644 --- a/config/locales/views/score_items/en.yml +++ b/config/locales/views/score_items/en.yml @@ -36,4 +36,3 @@ en: total_visibility: > Show or hide the calculated total score in Dodona for this exercise when the feedback is released. Individual score items are still visible if they are marked as such. - visibility: Show or hide this score item. diff --git a/config/locales/views/score_items/nl.yml b/config/locales/views/score_items/nl.yml index 9d8e8cfe1e..f9f39892ce 100644 --- a/config/locales/views/score_items/nl.yml +++ b/config/locales/views/score_items/nl.yml @@ -36,4 +36,3 @@ nl: total_visibility: > Toon of verberg de berekende totaalscore in Dodona voor deze oefening als de feedback vrijgegeven wordt. Individuele scoreonderdelen zijn nog wel zichtbaar als ze als zodanig gemarkeerd zijn. - visibility: Toon of verberg dit scoreonderdeel. From 40d20f9659240bf902abce77090fa90da1b4fbe4 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 8 Aug 2024 14:31:21 +0200 Subject: [PATCH 18/28] Fix row heights --- app/assets/javascripts/components/input_table.ts | 1 + app/views/score_items/_exercise.html.erb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index c2e3b92ef0..d0a86bb25a 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -108,6 +108,7 @@ export class ScoreItemInputTable extends DodonaElement { parseFormulas: false, selectionCopy: false, wordWrap: true, + defaultRowHeight: 30, }); // update description column width when the window is resized diff --git a/app/views/score_items/_exercise.html.erb b/app/views/score_items/_exercise.html.erb index 07d0a15040..fc3740e0f5 100644 --- a/app/views/score_items/_exercise.html.erb +++ b/app/views/score_items/_exercise.html.erb @@ -32,9 +32,9 @@ <%= format_score score_item.maximum %> <% if score_item.visible %> - + <% else %> - + <% end %> From 61ed5401388478a82a2ccb20fac1a78ec3180552 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 8 Aug 2024 15:17:59 +0200 Subject: [PATCH 19/28] Fix tests --- .../score_items_controller_test.rb | 86 ++++++++----------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/test/controllers/score_items_controller_test.rb b/test/controllers/score_items_controller_test.rb index 781fcc97e7..525add3b70 100644 --- a/test/controllers/score_items_controller_test.rb +++ b/test/controllers/score_items_controller_test.rb @@ -8,10 +8,11 @@ def setup sign_in @staff_member end - test 'should copy score items if course administrator' do - from = @evaluation.evaluation_exercises.first - create :score_item, evaluation_exercise: from - create :score_item, evaluation_exercise: from + test 'should update score item if course administrator' do + exercise = @evaluation.evaluation_exercises.first + score_item = create :score_item, evaluation_exercise: exercise, + description: 'Before test', + maximum: '10.0' [ [@staff_member, :success], @@ -19,57 +20,25 @@ def setup [create(:staff), :forbidden], [users(:zeus), :success], [nil, :unauthorized] - ].each do |user, expected| - to = create :evaluation_exercise, evaluation: @evaluation - sign_in user if user.present? - post copy_evaluation_score_items_path(@evaluation, format: :js), params: { - copy: { - from: from.id, - to: to.id - } - } - - assert_response expected - assert_equal 2, to.score_items.count if expected == :success - - sign_out user if user.present? - end - end - - test 'should add score items to all if course administrator' do - [ - [@staff_member, :ok], - [users(:student), :no], - [create(:staff), :no], - [users(:zeus), :ok], - [nil, :no] ].each do |user, expected| sign_in user if user.present? - post add_all_evaluation_score_items_path(@evaluation, format: :js), params: { + patch evaluation_score_item_path(@evaluation, score_item, format: :json), params: { score_item: { - name: 'Test', - description: 'Test', + description: 'After test', maximum: '20.0' } } - @evaluation.evaluation_exercises.reload - @evaluation.evaluation_exercises.each do |e| - if expected == :ok - assert_equal 1, e.score_items.length - e.update!(score_items: []) - end - - assert_empty e.score_items - end + + assert_response expected sign_out user if user.present? end end - test 'should update score item if course administrator' do + test 'should update all score items if course administrator' do exercise = @evaluation.evaluation_exercises.first - score_item = create :score_item, evaluation_exercise: exercise, - description: 'Before test', - maximum: '10.0' + score_items = create_list :score_item, 3, evaluation_exercise: exercise, + description: 'Before test', + maximum: '10.0' [ [@staff_member, :success], @@ -79,14 +48,33 @@ def setup [nil, :unauthorized] ].each do |user, expected| sign_in user if user.present? - patch evaluation_score_item_path(@evaluation, score_item, format: :json), params: { - score_item: { - description: 'After test', - maximum: '20.0' - } + patch evaluation_evaluation_exercise_score_items_path(@evaluation, exercise, format: :json), params: { + score_items: [ + { id: score_items[0].id, name: 'edited', description: 'After test', maximum: '20.0' }, + { name: 'new', description: 'new value', maximum: '25.0' } + ] } assert_response expected + + exercise.score_items.reload + if expected == :success + assert_equal 2, exercise.score_items.count + assert_equal 'After test', exercise.score_items.first.description + assert_in_delta(20.0, exercise.score_items.first.maximum) + assert_equal 'new', exercise.score_items.last.name + assert_equal 'new value', exercise.score_items.last.description + assert_in_delta(25.0, exercise.score_items.last.maximum) + + # reset + exercise.score_items.each(&:destroy) + score_items = create_list :score_item, 3, evaluation_exercise: exercise, + description: 'Before test', + maximum: '10.0' + else + assert_equal 3, exercise.score_items.count + end + sign_out user if user.present? end end From 66c5247fac5a456507b085c7d082d48ac0ed888f Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 8 Aug 2024 15:27:20 +0200 Subject: [PATCH 20/28] Remove system test --- test/system/score_items_test.rb | 39 --------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 test/system/score_items_test.rb diff --git a/test/system/score_items_test.rb b/test/system/score_items_test.rb deleted file mode 100644 index b8a1866f02..0000000000 --- a/test/system/score_items_test.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'capybara/minitest' -require 'application_system_test_case' - -class ScoreItemsTest < ApplicationSystemTestCase - include Devise::Test::IntegrationHelpers - # Make the Capybara DSL available in all integration tests - include Capybara::DSL - # Make `assert_*` methods behave like Minitest assertions - include Capybara::Minitest::Assertions - - def setup - @evaluation = create :evaluation, :with_submissions - @staff_member = create :staff - @evaluation.series.course.administrating_members << @staff_member - sign_in @staff_member - @exercise = @evaluation.evaluation_exercises.first - @score_item = create :score_item, evaluation_exercise: @exercise, - description: 'Before test', - maximum: '400.0', - visible: false - end - - test 'updating score item works' do - visit(edit_evaluation_path(id: @evaluation.id)) - - # Ensure we don't accidentally test nothing - assert_no_text '314.25' - - # Click the edit button of the score item - find("a[href=\"#edit-form-#{@score_item.id}\"]").click - # Change value of score item - find(id: "#{@exercise.id}_score_item_maximum").fill_in with: '314.25' - # Save our changes to the score item - find(id: "#{@exercise.id}_edit_score_item_#{@score_item.id}").find('input[type=submit]').click - - # Check that the score has been updated on the page - assert_text '314.25' - end -end From 2451a67761951cdc9d3165d197408c5b5ff0aeea Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 8 Aug 2024 15:35:28 +0200 Subject: [PATCH 21/28] Fix empty case --- .../javascripts/components/input_table.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index d0a86bb25a..87959ba4e7 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -48,13 +48,16 @@ export class ScoreItemInputTable extends DodonaElement { } get data(): CellData[][] { - return this.scoreItems.map(item => [ - item.id, - item.name, - item.description, - item.maximum, - item.visible - ]); + return [ + ...this.scoreItems.map(item => [ + item.id, + item.name, + item.description, + item.maximum, + item.visible + ]), + ["", "", "", "", false] + ]; } get editedScoreItems(): ScoreItem[] { From c68b4d0c95b00dc86b46758c8711155496b60a79 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 8 Aug 2024 15:42:51 +0200 Subject: [PATCH 22/28] Judge empty rows more kindly --- app/assets/javascripts/components/input_table.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index 87959ba4e7..8ee1b6e763 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -63,21 +63,19 @@ export class ScoreItemInputTable extends DodonaElement { get editedScoreItems(): ScoreItem[] { const tableData = this.table.getData(); - // Remove the last row if it is empty - if (tableData[tableData.length - 1].every(cell => cell === "" || cell === false)) { - tableData.pop(); - } - - return tableData.map((row: CellData[], index: number) => { + const scoreItems = tableData.map((row: CellData[], index: number) => { return { id: row[0] as number | null, name: row[1] as string, description: row[2] as string, maximum: row[3] as string, visible: row[4] as boolean, - order: index + order: index, }; }); + + // filter out empty rows + return scoreItems.filter(item => !(item.name === "" && item.maximum === "" && item.description === "" && item.visible === false)); } get columnConfig(): ColumnWithTooltip[] { From ad5c0d896db786a1fddccc5e3242adf4432110f5 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 8 Aug 2024 15:48:24 +0200 Subject: [PATCH 23/28] Remove the export option --- app/assets/javascripts/components/input_table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index 8ee1b6e763..a2ff614e58 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -104,12 +104,12 @@ export class ScoreItemInputTable extends DodonaElement { allowRenameColumn: false, columnResize: false, columnSorting: false, - csvFileName: "scoresheet", minSpareRows: 1, parseFormulas: false, selectionCopy: false, wordWrap: true, defaultRowHeight: 30, + allowExport: false, }); // update description column width when the window is resized From defc89e3c208fa2c4b2bea7073a1c8d086de8318 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 9 Aug 2024 09:51:40 +0200 Subject: [PATCH 24/28] Avoid type coercion --- app/assets/javascripts/components/input_table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index a2ff614e58..aaccc7ec47 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -134,7 +134,7 @@ export class ScoreItemInputTable extends DodonaElement { invalidCells.push("B" + row); } const max = parseFloat(item.maximum); - if (isNaN(max) || max <= 0) { + if (Number.isNaN(max) || max <= 0) { invalidCells.push("D" + row); } }); From 41d8faead29a8d8e1c316100277c00e0dc3e73a1 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 9 Aug 2024 09:52:33 +0200 Subject: [PATCH 25/28] Remove comments --- app/assets/javascripts/components/input_table.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index aaccc7ec47..5c4bbd1833 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -114,9 +114,7 @@ export class ScoreItemInputTable extends DodonaElement { // update description column width when the window is resized new ResizeObserver(() => { - // if (Math.abs(parseInt(this.table.getWidth(2) as string) - this.descriptionColWidth) > 10) { this.table.setWidth(2, this.descriptionColWidth); - // } }).observe(this.tableRef.value); } From bf47ad716c9bc6c4791e3c17c9953f1472f372a5 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 9 Aug 2024 09:54:11 +0200 Subject: [PATCH 26/28] Add docs --- app/assets/javascripts/components/input_table.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index 5c4bbd1833..b369b3c29b 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -19,7 +19,16 @@ type ScoreItem = { type ColumnWithTooltip = Column & { tooltip?: string }; - +/** + * A spreadsheet table to edit score items. + * + * @element d-score-item-input-table + * + * @fires cancel - When the cancel button is clicked. + * + * @prop {string} route - The route to send the updated score items to. + * @prop {ScoreItem[]} scoreItems - The original score items, that will be displayed in the table. + */ @customElement("d-score-item-input-table") export class ScoreItemInputTable extends DodonaElement { @property({ type: String }) From 973371df5038a93fb46dc1af8b0b871ab81f2935 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 9 Aug 2024 10:04:13 +0200 Subject: [PATCH 27/28] Add translations for menu --- .../javascripts/components/input_table.ts | 48 +++++++++++-------- app/assets/javascripts/i18n/translations.json | 14 ++++++ config/locales/js/en.yml | 6 +++ config/locales/js/nl.yml | 7 ++- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/components/input_table.ts b/app/assets/javascripts/components/input_table.ts index b369b3c29b..827ac6057c 100644 --- a/app/assets/javascripts/components/input_table.ts +++ b/app/assets/javascripts/components/input_table.ts @@ -3,7 +3,7 @@ import { html, PropertyValues, TemplateResult } from "lit"; import jspreadsheet, { Column, JspreadsheetInstance } from "jspreadsheet-ce"; import { createRef, ref, Ref } from "lit/directives/ref.js"; import { DodonaElement } from "components/meta/dodona_element"; -import { fetch } from "utilities"; +import { fetch, ready } from "utilities"; import { i18n } from "i18n/i18n"; import { Tooltip } from "bootstrap"; @@ -97,12 +97,21 @@ export class ScoreItemInputTable extends DodonaElement { ]; } - protected firstUpdated(_changedProperties: PropertyValues): void { - super.firstUpdated(_changedProperties); + async initTable(): Promise { + // Wait for translations to be present + await ready; + this.table = jspreadsheet(this.tableRef.value, { root: this, data: this.data, columns: this.columnConfig, + text: { + copy: i18n.t("js.score_items.jspreadsheet.copy"), + deleteSelectedRows: i18n.t("js.score_items.jspreadsheet.deleteSelectedRows"), + insertANewRowAfter: i18n.t("js.score_items.jspreadsheet.insertNewRowAfter"), + insertANewRowBefore: i18n.t("js.score_items.jspreadsheet.insertNewRowBefore"), + paste: i18n.t("js.score_items.jspreadsheet.paste"), + }, about: false, allowDeleteColumn: false, allowDeleteRow: true, @@ -121,12 +130,28 @@ export class ScoreItemInputTable extends DodonaElement { allowExport: false, }); + // init tooltips + this.columnConfig.forEach((column, index) => { + const td = this.tableRef.value.querySelector(`thead td[data-x="${index}"]`); + if (td && column.tooltip) { + td.setAttribute("title", column.tooltip); + new Tooltip(td); + } + }); + + // update description column width when the window is resized new ResizeObserver(() => { this.table.setWidth(2, this.descriptionColWidth); }).observe(this.tableRef.value); } + protected firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + + this.initTable(); + } + validate(): boolean { // Remove all error classes this.tableRef.value.querySelectorAll("td.error").forEach(cell => { @@ -193,18 +218,6 @@ export class ScoreItemInputTable extends DodonaElement { } } - updateTitlesAndTooltips(): void { - this.columnConfig.forEach((column, index) => { - this.table.setHeader(index, column.title); - - const td = this.tableRef.value.querySelector(`thead td[data-x="${index}"]`); - if (td && column.tooltip) { - td.setAttribute("title", column.tooltip); - new Tooltip(td); - } - }); - } - cancel(): void { if (this.table) { this.table.setData(this.data); @@ -214,11 +227,6 @@ export class ScoreItemInputTable extends DodonaElement { render(): TemplateResult { - if (this.table && this.tableRef.value) { - // Reset column headers as language might have changed - this.updateTitlesAndTooltips(); - } - return html` ${this.hasErrors ? html`
${i18n.t("js.score_items.validation_warning")}
` : ""}
diff --git a/app/assets/javascripts/i18n/translations.json b/app/assets/javascripts/i18n/translations.json index 448ee623a4..dea30abbd1 100644 --- a/app/assets/javascripts/i18n/translations.json +++ b/app/assets/javascripts/i18n/translations.json @@ -368,6 +368,13 @@ "deleted_warning": "You have deleted one or more score items. This will also delete all scores for these items.", "description": "Description", "description_help": "A description is optional. Markdown formatting can be used. This is visible to the students.", + "jspreadsheet": { + "copy": "Copy...", + "deleteSelectedRows": "Delete selected rows", + "insertNewRowAfter": "Insert new row after", + "insertNewRowBefore": "Insert new row before", + "paste": "Paste..." + }, "maximum": "Maximum", "maximum_help": "The maximum grade for this score item. The grade should be greater than 0, and works in increments of 0.25.", "modified_warning": "You have changed the maximum score of one or more score items. This will mark all completed evaluations with this score item as uncompleted.", @@ -887,6 +894,13 @@ "deleted_warning": "Je hebt een of meerdere scoreonderdelen verwijderd. Dit zal ook de bijhorende scores van de studenten verwijderen.", "description": "Beschrijving", "description_help": "Een beschrijving is optioneel en kan in Markdown geschreven worden. Dit is zichtbaar voor de studenten.", + "jspreadsheet": { + "copy": "Kopiëer...", + "deleteSelectedRows": "Verwijder geselecteerde rijen", + "insertNewRowAfter": "Voeg nieuwe rij toe na deze", + "insertNewRowBefore": "Voeg nieuwe rij toe voor deze", + "paste": "Plak..." + }, "maximum": "Maximum", "maximum_help": "De maximumscore voor dit scoreonderdeel. Dit moet een positief getal zijn en gaat in stappen van 0.25.", "modified_warning": "Je hebt de maximumscore van een of meerdere scoreonderdelen aangepast. Alle afgewerkte evaluaties met dit scoreonderdeel zullen terug als onafgewerkt gemarkeerd worden.", diff --git a/config/locales/js/en.yml b/config/locales/js/en.yml index f6a0a15b1f..d4d6b30ed1 100644 --- a/config/locales/js/en.yml +++ b/config/locales/js/en.yml @@ -318,3 +318,9 @@ en: validation_warning: All score items must have a name and a maximum score, and the maximum score must be greater than 0. save: Save cancel: Cancel + jspreadsheet: + copy: Copy... + deleteSelectedRows: Delete selected rows + insertNewRowAfter: Insert new row after + insertNewRowBefore: Insert new row before + paste: Paste... diff --git a/config/locales/js/nl.yml b/config/locales/js/nl.yml index ee1348306b..c2c528da14 100644 --- a/config/locales/js/nl.yml +++ b/config/locales/js/nl.yml @@ -318,5 +318,10 @@ nl: modified_warning: "Je hebt de maximumscore van een of meerdere scoreonderdelen aangepast. Alle afgewerkte evaluaties met dit scoreonderdeel zullen terug als onafgewerkt gemarkeerd worden." deleted_warning: "Je hebt een of meerdere scoreonderdelen verwijderd. Dit zal ook de bijhorende scores van de studenten verwijderen." validation_warning: "Alle scoreonderdelen moeten een naam en een maximumscore hebben. De maximumscore moet een positief getal zijn." - + jspreadsheet: + copy: Kopiëer... + deleteSelectedRows: Verwijder geselecteerde rijen + insertNewRowAfter: Voeg nieuwe rij toe na deze + insertNewRowBefore: Voeg nieuwe rij toe voor deze + paste: Plak... From a7e363db6654fcc138e4f7ff83a9ebeb1194d149 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 9 Aug 2024 11:36:49 +0200 Subject: [PATCH 28/28] Fix dark mode --- app/assets/stylesheets/base.css.scss | 1 + .../stylesheets/theme/jspreadsheet.css.scss | 201 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 app/assets/stylesheets/theme/jspreadsheet.css.scss diff --git a/app/assets/stylesheets/base.css.scss b/app/assets/stylesheets/base.css.scss index 6b61dc6545..36e4c23586 100644 --- a/app/assets/stylesheets/base.css.scss +++ b/app/assets/stylesheets/base.css.scss @@ -73,6 +73,7 @@ } // 5. Add additional custom code here +@import "theme/jspreadsheet.css.scss"; @import "bootstrap_style_overrides.css.scss"; @import "libraries.css.scss"; @import "material_icons.css.scss"; diff --git a/app/assets/stylesheets/theme/jspreadsheet.css.scss b/app/assets/stylesheets/theme/jspreadsheet.css.scss new file mode 100644 index 0000000000..414b53318e --- /dev/null +++ b/app/assets/stylesheets/theme/jspreadsheet.css.scss @@ -0,0 +1,201 @@ +// Overwrites colors in node_modules/jspreadsheet-ce/dist/jspreadsheet.css + +:root { + --jexcel-border-color: var(--d-on-surface); +} + +/* stylelint-disable-next-line selector-class-pattern */ +.jexcel_container.fullscreen .jexcel_content { + background-color: var(--d-surface); +} + +/* stylelint-disable-next-line selector-class-pattern */ +.jexcel_content::-webkit-scrollbar-track { + background: var(--d-background); +} + + +.jexcel { + background-color: var(--d-surface); + border-top: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid var(--d-divider); + border-bottom: 1px solid var(--d-divider); +} + +.jexcel > thead > tr > td { + border-top: 1px solid var(--d-divider); + border-left: 1px solid var(--d-divider); + border-right: 1px solid transparent; + border-bottom: 1px solid transparent; + background-color: var(--d-code-bg); +} + +.jexcel > thead > tr > td.dragging { + background-color: var(--d-surface); +} + +.jexcel > thead > tr > td.selected { + background-color: rgba(var(--d-on-surface-rgb), 0.25); +} + +.jexcel > tbody > tr > td:first-child { + background-color: var(--d-code-bg); +} + +.jexcel > tbody > tr.dragging > td { + background-color: var(--d-background); +} + +.jexcel > tbody > tr > td { + border-top: 1px solid var(--d-divider); + border-left: 1px solid var(--d-divider); + border-right: 1px solid transparent; + border-bottom: 1px solid transparent; +} + +.jexcel > tbody > tr > td.readonly { + color: rgba(var(--d-on-surface-rgb), 0.3); +} + +.jexcel > tbody > tr.selected > td:first-child { + background-color: rgba(var(--d-on-surface-rgb), 0.25); +} + +.jexcel > tbody > tr > td.highlight > a { + color: var(--d-info); +} + +.jexcel > tfoot > tr > td { + border-top: 1px solid var(--d-divider); + border-left: 1px solid var(--d-divider); + border-right: 1px solid transparent; + border-bottom: 1px solid transparent; + background-color: var(--d-code-bg); +} + +.jexcel .highlight { + background-color: rgba(var(--d-on-surface-rgb), 0.05); +} + +.jexcel .highlight-top { + border-top: 1px solid var(--jexcel-border-color); + box-shadow: 0 -1px var(--d-divider); +} + +.jexcel .highlight-left { + border-left: 1px solid var(--jexcel-border-color); + box-shadow: -1px 0 var(--d-divider); +} + +.jexcel .highlight-right { + border-right: 1px solid var(--jexcel-border-color); +} + +.jexcel .highlight-bottom { + border-bottom: 1px solid var(--jexcel-border-color); +} + +.jexcel .highlight-top.highlight-left { + box-shadow: -1px -1px var(--d-divider); +} + +.jexcel .highlight-selected { + background-color: rgba(var(--d-on-surface-rgb), 0.0); +} + +.jexcel .selection { + background-color: rgba(var(--d-on-surface-rgb), 0.05); +} + +.jexcel .selection-left { + border-left: 1px dotted var(--jexcel-border-color); +} + +.jexcel .selection-right { + border-right: 1px dotted var(--jexcel-border-color); +} + +.jexcel .selection-top { + border-top: 1px dotted var(--jexcel-border-color); +} + +.jexcel .selection-bottom { + border-bottom: 1px dotted var(--jexcel-border-color); +} + +.jexcel .editor .jupload { + background-color: var(--d-surface); +} + +/* stylelint-disable-next-line selector-class-pattern */ +.jexcel .editor .jexcel_richtext { + background-color: var(--d-surface); +} + +.jexcel .editor .jclose::after { + text-shadow: 0 0 5px var(--d-surface); +} + +.jexcel .error { + border: 1px solid var(--d-danger); +} + +.jexcel .copying-top { + border-top: 1px dashed var(--jexcel-border-color); +} + +.jexcel .copying-left { + border-left: 1px dashed var(--jexcel-border-color); +} + +.jexcel .copying-right { + border-right: 1px dashed var(--jexcel-border-color); +} + +.jexcel .copying-bottom { + border-bottom: 1px dashed var(--jexcel-border-color); +} + +.red { + color: var(--d-danger); +} + +// overwrites colors and pading for jcontextmenu defined in node_modules/jsuites/dist/jsuites.css + +.jcontextmenu { + background: var(--d-surface); + color: var(--d-on-surface); + border: 1px solid var(--d-divider); + border-radius: 5px; + padding: 0; + + @include shadow-z3; +} + +.jcontextmenu > div { + padding: 8px 16px; + width: auto; +} + +.jcontextmenu > div a { + color: var(--d-on-surface); +} + +.jcontextmenu .jcontextmenu-disabled a { + color: var(--d-on-surface-muted); +} + +.jcontextmenu .jcontextmenu-disabled::before { + color: var(--d-on-surface-muted); +} + +.jcontextmenu > div:hover { + background: var(--d-secondary-container); + color: var(--d-on-secondary-container); +} + +.jcontextmenu hr { + border: 1px solid var(--d-divider); + margin: 0; +}