From e7326c026358da3b3bed5262fe98447be40ab563 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 24 Mar 2022 15:39:02 +0100 Subject: [PATCH 01/73] Add lit --- .../javascripts/components/sort_button.ts | 0 babel.config.js | 1 + package.json | 2 + yarn.lock | 70 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 app/assets/javascripts/components/sort_button.ts diff --git a/app/assets/javascripts/components/sort_button.ts b/app/assets/javascripts/components/sort_button.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/babel.config.js b/babel.config.js index 2ca6b6b065..67e01e629f 100644 --- a/babel.config.js +++ b/babel.config.js @@ -42,6 +42,7 @@ module.exports = function (api) { isTestEnv && "babel-plugin-dynamic-import-node", "@babel/plugin-transform-destructuring", ["@babel/plugin-proposal-class-properties", { loose: true }], + ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], ["@babel/plugin-proposal-private-methods", { loose: true }], ["@babel/plugin-proposal-object-rest-spread", { useBuiltIns: true }], ["@babel/plugin-proposal-private-property-in-object", { loose: true }], diff --git a/package.json b/package.json index a9076d8e15..cfe74eb079 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@babel/core": "^7.17.8", + "@babel/plugin-proposal-decorators": "^7.17.8", "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", "@babel/preset-typescript": "^7.16.7", @@ -31,6 +32,7 @@ "glob": "^7.2.0", "iframe-resizer": "^4.3.2", "jquery": "^3.6.0", + "lit": "2.2.1", "moment": "^2.29.1", "sass": "^1.49.9", "typeahead.js": "^0.11.1", diff --git a/yarn.lock b/yarn.lock index adcc6a706e..4b8afc27e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -99,6 +99,19 @@ "@babel/helper-replace-supers" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" +"@babel/helper-create-class-features-plugin@^7.17.6": + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" + integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-create-regexp-features-plugin@^7.16.7": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" @@ -331,6 +344,17 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" +"@babel/plugin-proposal-decorators@^7.17.8": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.8.tgz#4f0444e896bee85d35cf714a006fc5418f87ff00" + integrity sha512-U69odN4Umyyx1xO1rTII0IDkAEC+RNlcKXtqOblfpzqy1C+aOplb76BQNq0+XdpVkOaPlpEDwd++joY8FNFJKA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.17.6" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/plugin-syntax-decorators" "^7.17.0" + charcodes "^0.2.0" + "@babel/plugin-proposal-dynamic-import@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" @@ -461,6 +485,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-syntax-decorators@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz#a2be3b2c9fe7d78bd4994e790896bc411e2f166d" + integrity sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -1215,6 +1246,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@lit/reactive-element@^1.3.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.3.1.tgz#3021ad0fa30a75a41212c5e7f1f169c5762ef8bb" + integrity sha512-nOJARIr3pReqK3hfFCSW2Zg/kFcFsSAlIE7z4a0C9D2dPrgD/YSn3ZP2ET/rxKB65SXyG7jJbkynBRm+tGlacw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1622,6 +1658,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/trusted-types@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -2342,6 +2383,11 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +charcodes@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4" + integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ== + "chokidar@>=3.0.0 <4.0.0": version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -4565,6 +4611,30 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +lit-element@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.0.tgz#9c981c55dfd9a8f124dc863edb62cc529d434db7" + integrity sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g== + dependencies: + "@lit/reactive-element" "^1.3.0" + lit-html "^2.2.0" + +lit-html@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.2.1.tgz#762f112a8b54eaf0bbae3f516de935a25dcc12d1" + integrity sha512-AiJ/Rs0awjICs2FioTnHSh+Np5dhYSkyRczKy3wKjp8qjLhr1Ov+GiHrUQNdX8ou1LMuznpIME990AZsa/tR8g== + dependencies: + "@types/trusted-types" "^2.0.2" + +lit@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/lit/-/lit-2.2.1.tgz#4b679e1d8cb6c7977b64921b1ea3eca7850ca1dd" + integrity sha512-dSe++R50JqrvNGXmI9OE13de1z5U/Y3J2dTm/9GC86vedI8ILoR8ZGnxfThFpvQ9m0lR0qRnIR4IiKj/jDCfYw== + dependencies: + "@lit/reactive-element" "^1.3.0" + lit-element "^3.2.0" + lit-html "^2.2.0" + loader-runner@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" From a527d22a3dacd97a151a5274b262d90eb1f3e3b6 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 24 Mar 2022 17:12:03 +0100 Subject: [PATCH 02/73] Create frontend sortbutons --- .../javascripts/components/sort_button.ts | 66 +++++++++++++++++++ .../stylesheets/material_icons.css.scss | 14 ++++ app/javascript/packs/application_pack.js | 3 + .../submissions/_submissions_table.html.erb | 12 ++-- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/components/sort_button.ts b/app/assets/javascripts/components/sort_button.ts index e69de29bb2..eba2a7ffaa 100644 --- a/app/assets/javascripts/components/sort_button.ts +++ b/app/assets/javascripts/components/sort_button.ts @@ -0,0 +1,66 @@ +import { html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +export class SortQuery { + active_column: string; + ascending: boolean; + buttons: Array = []; + + registerSortButton(b: SortButton): void { + this.buttons.push(b); + } + + notifySortButtons(): void { + this.buttons.forEach(b => { + b.active_column = this.active_column; + b.ascending = this.ascending; + }); + } + + sortBy(column: string, ascending: boolean): void { + this.active_column = column; + this.ascending = ascending; + console.log(this.active_column, this.ascending ? "ASC" : "DESC"); + this.notifySortButtons(); + } +} + +dodona.sort_query = new SortQuery(); + +@customElement("dodona-sort-button") +export class SortButton extends LitElement { + @property({ type: String }) + column: string; + @property({ type: String }) + active_column: string; + @property({ type: Boolean }) + ascending: boolean; + + // don't use shadow dom + createRenderRoot(): Element { + return this; + } + + isActive(): boolean { + return this.column === this.active_column; + } + + getSortIcon(): string { + return this.isActive() ? this.ascending ? "sort-ascending" : "sort-descending" : "sort"; + } + + sort(): void { + dodona.sort_query.sortBy(this.column, !this.isActive() || !this.ascending); + } + + constructor() { + super(); + dodona.sort_query.registerSortButton(this); + } + + render(): TemplateResult { + return html` + this.sort()}> + `; + } +} diff --git a/app/assets/stylesheets/material_icons.css.scss b/app/assets/stylesheets/material_icons.css.scss index 73e45ff03e..cacdd86520 100644 --- a/app/assets/stylesheets/material_icons.css.scss +++ b/app/assets/stylesheets/material_icons.css.scss @@ -8,6 +8,11 @@ line-height: 14px; } +.mdi.mdi-16::before { + font-size: 16px; + line-height: 18px; +} + .mdi.mdi-18::before { font-size: 18px; line-height: 20px; @@ -145,6 +150,15 @@ a.btn-fab .mdi::before { } } +.sort-icon { + color: $text-secondary; + cursor: pointer; + + &:hover { + color: $text-color; + } +} + .custom-material-icons { display: inline-block; width: 24px; diff --git a/app/javascript/packs/application_pack.js b/app/javascript/packs/application_pack.js index 30bb8f420f..c50a67601f 100644 --- a/app/javascript/packs/application_pack.js +++ b/app/javascript/packs/application_pack.js @@ -27,6 +27,9 @@ const bootstrap = { Alert, Button, Collapse, Dropdown, Modal, Popover, Tab, Tool window.bootstrap = bootstrap; import "polyfills.js"; + +import "components/sort_button.ts"; + import { Drawer } from "drawer"; import { Toast } from "toast"; import { Notification } from "notification"; diff --git a/app/views/submissions/_submissions_table.html.erb b/app/views/submissions/_submissions_table.html.erb index 10720945df..c2642d7c25 100644 --- a/app/views/submissions/_submissions_table.html.erb +++ b/app/views/submissions/_submissions_table.html.erb @@ -3,16 +3,16 @@ - + <% if user.nil? && (current_user.admin? || current_user.administrating_courses.any?) %> - <%= t ".user" %> + <%= t ".user" %> <% end %> <% unless exercise.present? %> - <%= t ".exercise" %> + <%= t ".exercise" %> <% end %> - <%= t ".time" %> - <%= t ".status" %> - <%= t ".summary" %> + <%= t ".time" %> + <%= t ".status" %> + <%= t ".summary" %> From a5fac1e13dbf79dd075a62f4022efe3682db4ce4 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 25 Mar 2022 10:28:25 +0100 Subject: [PATCH 03/73] Allow sorting in the backend --- app/controllers/submissions_controller.rb | 6 ++++++ app/models/submission.rb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index aef513c0dd..3dddef933f 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -21,6 +21,12 @@ class SubmissionsController < ApplicationController end end + has_scope :order_by_number + has_scope :order_by_user + has_scope :order_by_exercise + has_scope :order_by_created_at + has_scope :order_by_status + content_security_policy only: %i[show] do |policy| # allow sandboxed tutor policy.frame_src -> { [sandbox_url] } diff --git a/app/models/submission.rb b/app/models/submission.rb index d98f36e858..1fef5e67e8 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -98,6 +98,12 @@ class Submission < ApplicationRecord correct.group(:exercise_id, :user_id).least_recent } + scope :order_by_number, ->(direction) { reorder number: direction, id: :desc } + scope :order_by_user, ->(direction) { includes(:user).reorder 'user.first_name': direction, id: :desc } + scope :order_by_exercise, ->(direction) { includes(:exercise).reorder 'activities.name_nl': direction, id: :desc } + scope :order_by_created_at, ->(direction) { reorder 'created_at': direction, id: :desc } + scope :order_by_status, ->(direction) { reorder 'status': direction, id: :desc } + def initialize(params) raise 'please explicitly tell whether you want to evaluate this submission' unless params.key? :evaluate From b52e6ea50c08b1c6f9bee55bbdad8a5834b62134 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Wed, 30 Mar 2022 09:34:27 +0200 Subject: [PATCH 04/73] Refactor index.js to query param state with listeners --- .../javascripts/components/search_actions.ts | 148 ++++++++++++++++++ .../javascripts/components/sort_button.ts | 48 +++++- app/assets/javascripts/search.ts | 98 ++++++++++++ 3 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/components/search_actions.ts create mode 100644 app/assets/javascripts/search.ts diff --git a/app/assets/javascripts/components/search_actions.ts b/app/assets/javascripts/components/search_actions.ts new file mode 100644 index 0000000000..33616bb404 --- /dev/null +++ b/app/assets/javascripts/components/search_actions.ts @@ -0,0 +1,148 @@ +import { html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { Toast } from "toast"; +import { fetch } from "util.js"; + +type SearchOption = {search: Record, type: string, text: string}; +type SearchAction = { + url: string, + type: string, + text: string, + action: string, + js: string, + confirm: string, + icon: string +}; + +@customElement("dodona-search-option") +export class SearchOptionElement extends LitElement { + @property() + searchOption: SearchOption; + @property() + key: number; + @state() + private _active = false; + + // don't use shadow dom + createRenderRoot(): Element { + return this; + } + + constructor() { + super(); + Object.keys(this.searchOption.search).forEach(k => { + dodona.search_query.query_params.subscribeByKey(k, () => this.setActive()); + }); + } + + setActive(): void { + this._active = Object.entries(this.searchOption.search).every(([key, value]) => { + return dodona.search_query.query_params.params.get(key) == value; + }); + } + + performSearch(): void { + Object.entries(this.searchOption.search).forEach(([key, value]) => { + dodona.search_query.query_params.updateParam(key, value); + }); + } + + render(): TemplateResult { + return html` +
  • +
    + + +
    +
  • + `; + } +} + +@customElement("dodona-search-actions") +export class SearchActions extends LitElement { + @property() + searchOptions: Array; + @property() + searchActions: Array; + + // don't use shadow dom + createRenderRoot(): Element { + return this; + } + + performAction(action: SearchAction): boolean { + if (!action.action && !action.js) { + return true; + } + + if (!action.action) { + eval(action.js); + return false; + } + + if (action.confirm === undefined || window.confirm(action.confirm)) { + const url: string = dodona.search_query.addParametersToUrl(action.action); + + fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" } + }).then( data => { + new Toast(data.message); + if (data.js) { + eval(data.js); + } else { + dodona.search_query.resetAllQueryParams(); + } + }); + } + + return false; + } + + + render(): TemplateResult { + return html` + + `; + } +} + + diff --git a/app/assets/javascripts/components/sort_button.ts b/app/assets/javascripts/components/sort_button.ts index eba2a7ffaa..dc0aecf314 100644 --- a/app/assets/javascripts/components/sort_button.ts +++ b/app/assets/javascripts/components/sort_button.ts @@ -1,3 +1,4 @@ +import "search.ts"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; @@ -6,6 +7,28 @@ export class SortQuery { ascending: boolean; buttons: Array = []; + constructor() { + dodona.search_query.query_params.subscribe((k, o, n) => { + if ( + k.startsWith("sort_by_") && + !(k === this.getQueryKey() && n === this.getQueryValue()) && + (n === "ASC" || n === "DESC") + ) { + this.active_column = k.substring(8); + this.ascending = n === "ASC"; + this.notifySortButtons(); + } + }); + } + + getQueryKey(): string { + return "sort_by_" + this.active_column; + } + + getQueryValue(): string { + return this.ascending ? "ASC" : "DESC"; + } + registerSortButton(b: SortButton): void { this.buttons.push(b); } @@ -18,22 +41,29 @@ export class SortQuery { } sortBy(column: string, ascending: boolean): void { - this.active_column = column; + if (this.active_column === column && this.ascending === ascending){ + return; + } + + if (this.active_column !== column) { + dodona.search_query.query_params.updateParam(this.getQueryKey(), undefined); + this.active_column = column; + } this.ascending = ascending; - console.log(this.active_column, this.ascending ? "ASC" : "DESC"); this.notifySortButtons(); + dodona.search_query.query_params.updateParam(this.getQueryKey(), this.getQueryValue()); } } - -dodona.sort_query = new SortQuery(); +dodona.state = {}; +dodona.state.sort_query = new SortQuery(); @customElement("dodona-sort-button") export class SortButton extends LitElement { @property({ type: String }) column: string; - @property({ type: String }) + // @state() active_column: string; - @property({ type: Boolean }) + // @state() ascending: boolean; // don't use shadow dom @@ -50,12 +80,14 @@ export class SortButton extends LitElement { } sort(): void { - dodona.sort_query.sortBy(this.column, !this.isActive() || !this.ascending); + dodona.state.sort_query.sortBy(this.column, !this.isActive() || !this.ascending); } constructor() { super(); - dodona.sort_query.registerSortButton(this); + dodona.state.sort_query.registerSortButton(this); + this.ascending = dodona.state.sort_query.ascending; + this.active_column = dodona.state.sort_query.active_column; } render(): TemplateResult { diff --git a/app/assets/javascripts/search.ts b/app/assets/javascripts/search.ts new file mode 100644 index 0000000000..6996184258 --- /dev/null +++ b/app/assets/javascripts/search.ts @@ -0,0 +1,98 @@ +import { fetch, updateArrayURLParameter, updateURLParameter } from "util.js"; + + +export class QueryParameters { + params: Map = new Map(); + listeners_by_key: Mapvoid>> = new Map(); + listeners: Array<(k: string, o: T, n: T)=>void> = []; + + resetParams(): void { + this.params.forEach((v, k) => { + if (v !== undefined) { + this.updateParam(k, undefined); + } + }); + } + + updateParam(key: string, value: T ): void { + console.log(key, value); + const old: T = this.params.get(key); + this.params.set(key, value); + + this.listeners.forEach(f => f(key, old, value)); + const listeners = this.listeners_by_key.get(key); + if (listeners) { + listeners.forEach(f => f(key, old, value)); + } + } + + subscribeByKey(key: string, listener: (k: string, o: T, n: T)=>void): void { + const listeners = this.listeners_by_key.get(key); + if (listeners) { + listeners.push(listener); + } else { + this.listeners_by_key.set(key, [listener]); + } + } + + subscribe(listener: (k: string, o: T, n: T)=>void): void { + this.listeners.push(listener); + } +} + +export class SearchQuery { + baseUrl: string; + searchIndex = 0; + appliedIndex = 0; + array_query_params: QueryParameters = new QueryParameters(); + query_params: QueryParameters = new QueryParameters(); + + constructor(baseUrl?: string) { + this.baseUrl = baseUrl || window.location.href; + this.array_query_params.subscribe(this.search); + this.query_params.subscribe(k => this.search(k)); + } + + addParametersToUrl(baseUrl?: string): string { + let url: string = baseUrl || this.baseUrl; + this.query_params.params.forEach((v, k) => url = updateURLParameter(url, k, v)); + this.array_query_params.params.forEach((v, k) => url = updateArrayURLParameter(url, k, v)); + + return url; + } + + resetAllQueryParams(): void { + this.query_params.resetParams(); + this.array_query_params.resetParams(); + } + + search(key?: string): void { + if (key === "page") { + return; + } + this.query_params.updateParam("page", "1"); + + const url = this.addParametersToUrl(); + const localIndex = ++this.searchIndex; + + // TODO CHECK REASON FOR if (updateAddressBar) + window.history.replaceState(null, "Dodona", url); + document.getElementById("progress-filter").style.visibility = "visible"; + fetch(updateURLParameter(url, "format", "js"), { + headers: { + "accept": "text/javascript" + }, + credentials: "same-origin", + }) + .then(resp => resp.text()) + .then(data => { + if (this.appliedIndex < localIndex) { + this.appliedIndex = localIndex; + eval(data); + } + document.getElementById("progress-filter").style.visibility = "hidden"; + }); + } +} + +dodona.search_query = new SearchQuery(); From e8f62209423486f675f8906f1b3262015284d91f Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Wed, 30 Mar 2022 10:18:18 +0200 Subject: [PATCH 05/73] Initialise query params on page refresh --- app/assets/javascripts/components/sort_button.ts | 11 +++++++++++ app/assets/javascripts/search.ts | 13 +++++++++++-- tsconfig.json | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/components/sort_button.ts b/app/assets/javascripts/components/sort_button.ts index dc0aecf314..bfd39b20b2 100644 --- a/app/assets/javascripts/components/sort_button.ts +++ b/app/assets/javascripts/components/sort_button.ts @@ -8,6 +8,17 @@ export class SortQuery { buttons: Array = []; constructor() { + const sortParams = [...dodona.search_query.query_params.params.entries()].filter( + ([k, v]) => k.startsWith("sort_by_") && (v=== "ASC" || v === "DESC") + ); + + if (sortParams.length > 0) { + this.active_column = sortParams[0][0].substring(8); + this.ascending = sortParams[0][1] === "ASC"; + sortParams.slice(1).forEach(([k, _]) => { + dodona.search_query.query_params.updateParam(k, undefined); + }); + } dodona.search_query.query_params.subscribe((k, o, n) => { if ( k.startsWith("sort_by_") && diff --git a/app/assets/javascripts/search.ts b/app/assets/javascripts/search.ts index 6996184258..3198fe3bb8 100644 --- a/app/assets/javascripts/search.ts +++ b/app/assets/javascripts/search.ts @@ -48,8 +48,17 @@ export class SearchQuery { query_params: QueryParameters = new QueryParameters(); constructor(baseUrl?: string) { - this.baseUrl = baseUrl || window.location.href; - this.array_query_params.subscribe(this.search); + const _url = baseUrl || window.location.href; + const url = new URL(_url.replace(/%5B%5D/g, "[]"), window.location.origin); + this.baseUrl = url.href; + for (const key of url.searchParams.keys()) { + if (key.endsWith("[]")) { + this.array_query_params.updateParam(key.substring(0, -2), url.searchParams.getAll(key)); + } else { + this.query_params.updateParam(key, url.searchParams.get(key)); + } + } + this.array_query_params.subscribe(k => this.search(k)); this.query_params.subscribe(k => this.search(k)); } diff --git a/tsconfig.json b/tsconfig.json index 82a0400ef3..94b468e5e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "moduleResolution": "node", "sourceMap": true, "target": "es5", + "downlevelIteration": true, "baseUrl": "app", "paths": { "*": ["assets/javascripts/*"] From b5ded393aba19d0065dc554b91d5bd944af24b0d Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Wed, 30 Mar 2022 10:51:16 +0200 Subject: [PATCH 06/73] Allow sort to interact correctly with backend --- app/assets/javascripts/components/sort_button.ts | 11 ++++++----- app/assets/javascripts/search.ts | 8 ++++---- app/models/submission.rb | 2 +- app/views/submissions/_submissions_table.html.erb | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/components/sort_button.ts b/app/assets/javascripts/components/sort_button.ts index bfd39b20b2..7ab5df8207 100644 --- a/app/assets/javascripts/components/sort_button.ts +++ b/app/assets/javascripts/components/sort_button.ts @@ -9,23 +9,24 @@ export class SortQuery { constructor() { const sortParams = [...dodona.search_query.query_params.params.entries()].filter( - ([k, v]) => k.startsWith("sort_by_") && (v=== "ASC" || v === "DESC") + ([k, v]) => k.startsWith("order_by_") && (v=== "ASC" || v === "DESC") ); if (sortParams.length > 0) { - this.active_column = sortParams[0][0].substring(8); + this.active_column = sortParams[0][0].substring(9); this.ascending = sortParams[0][1] === "ASC"; + console.log(this.active_column, this.ascending); sortParams.slice(1).forEach(([k, _]) => { dodona.search_query.query_params.updateParam(k, undefined); }); } dodona.search_query.query_params.subscribe((k, o, n) => { if ( - k.startsWith("sort_by_") && + k.startsWith("order_by_") && !(k === this.getQueryKey() && n === this.getQueryValue()) && (n === "ASC" || n === "DESC") ) { - this.active_column = k.substring(8); + this.active_column = k.substring(9); this.ascending = n === "ASC"; this.notifySortButtons(); } @@ -33,7 +34,7 @@ export class SortQuery { } getQueryKey(): string { - return "sort_by_" + this.active_column; + return "order_by_" + this.active_column; } getQueryValue(): string { diff --git a/app/assets/javascripts/search.ts b/app/assets/javascripts/search.ts index 3198fe3bb8..40bafbb84d 100644 --- a/app/assets/javascripts/search.ts +++ b/app/assets/javascripts/search.ts @@ -1,4 +1,4 @@ -import { fetch, updateArrayURLParameter, updateURLParameter } from "util.js"; +import { createDelayer, fetch, updateArrayURLParameter, updateURLParameter } from "util.js"; export class QueryParameters { @@ -15,7 +15,6 @@ export class QueryParameters { } updateParam(key: string, value: T ): void { - console.log(key, value); const old: T = this.params.get(key); this.params.set(key, value); @@ -58,8 +57,9 @@ export class SearchQuery { this.query_params.updateParam(key, url.searchParams.get(key)); } } - this.array_query_params.subscribe(k => this.search(k)); - this.query_params.subscribe(k => this.search(k)); + const delay = createDelayer(); + this.array_query_params.subscribe(k => delay(() => this.search(k), 100)); + this.query_params.subscribe(k => delay(() => this.search(k), 100)); } addParametersToUrl(baseUrl?: string): string { diff --git a/app/models/submission.rb b/app/models/submission.rb index 1fef5e67e8..e54aba70cf 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -99,7 +99,7 @@ class Submission < ApplicationRecord } scope :order_by_number, ->(direction) { reorder number: direction, id: :desc } - scope :order_by_user, ->(direction) { includes(:user).reorder 'user.first_name': direction, id: :desc } + scope :order_by_user, ->(direction) { includes(:user).reorder 'users.first_name': direction, 'users.last_name': direction, id: :desc } scope :order_by_exercise, ->(direction) { includes(:exercise).reorder 'activities.name_nl': direction, id: :desc } scope :order_by_created_at, ->(direction) { reorder 'created_at': direction, id: :desc } scope :order_by_status, ->(direction) { reorder 'status': direction, id: :desc } diff --git a/app/views/submissions/_submissions_table.html.erb b/app/views/submissions/_submissions_table.html.erb index c2642d7c25..6b12147e97 100644 --- a/app/views/submissions/_submissions_table.html.erb +++ b/app/views/submissions/_submissions_table.html.erb @@ -10,9 +10,9 @@ <% unless exercise.present? %> <%= t ".exercise" %> <% end %> - <%= t ".time" %> + <%= t ".time" %> <%= t ".status" %> - <%= t ".summary" %> + <%= t ".summary" %> From 8b6a456a639119e3a1362164c832335bc5879199 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Wed, 30 Mar 2022 11:04:19 +0200 Subject: [PATCH 07/73] Allow removing sort and make listerners more abstract --- .../javascripts/components/sort_button.ts | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/components/sort_button.ts b/app/assets/javascripts/components/sort_button.ts index 7ab5df8207..b35e6c667d 100644 --- a/app/assets/javascripts/components/sort_button.ts +++ b/app/assets/javascripts/components/sort_button.ts @@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators.js"; export class SortQuery { active_column: string; ascending: boolean; - buttons: Array = []; + listeners: Array<(c: string, a: boolean) => void> = []; constructor() { const sortParams = [...dodona.search_query.query_params.params.entries()].filter( @@ -28,7 +28,7 @@ export class SortQuery { ) { this.active_column = k.substring(9); this.ascending = n === "ASC"; - this.notifySortButtons(); + this.notify(); } }); } @@ -41,19 +41,16 @@ export class SortQuery { return this.ascending ? "ASC" : "DESC"; } - registerSortButton(b: SortButton): void { - this.buttons.push(b); + subscribe(listener: (c: string, a: boolean) => void): void { + this.listeners.push(listener); } - notifySortButtons(): void { - this.buttons.forEach(b => { - b.active_column = this.active_column; - b.ascending = this.ascending; - }); + notify(): void { + this.listeners.forEach(f => f(this.active_column, this.ascending)); } sortBy(column: string, ascending: boolean): void { - if (this.active_column === column && this.ascending === ascending){ + if (this.active_column === column && this.ascending === ascending) { return; } @@ -62,8 +59,10 @@ export class SortQuery { this.active_column = column; } this.ascending = ascending; - this.notifySortButtons(); - dodona.search_query.query_params.updateParam(this.getQueryKey(), this.getQueryValue()); + this.notify(); + if (this.active_column) { + dodona.search_query.query_params.updateParam(this.getQueryKey(), this.getQueryValue()); + } } } dodona.state = {}; @@ -73,10 +72,9 @@ dodona.state.sort_query = new SortQuery(); export class SortButton extends LitElement { @property({ type: String }) column: string; - // @state() - active_column: string; - // @state() - ascending: boolean; + + active_column: string; + ascending: boolean; // don't use shadow dom createRenderRoot(): Element { @@ -92,14 +90,23 @@ export class SortButton extends LitElement { } sort(): void { - dodona.state.sort_query.sortBy(this.column, !this.isActive() || !this.ascending); + if (!this.isActive()) { + dodona.state.sort_query.sortBy(this.column, true); + } else if (this.ascending) { + dodona.state.sort_query.sortBy(this.column, false); + } else { + dodona.state.sort_query.sortBy(undefined, undefined); + } } constructor() { super(); - dodona.state.sort_query.registerSortButton(this); this.ascending = dodona.state.sort_query.ascending; this.active_column = dodona.state.sort_query.active_column; + dodona.state.sort_query.subscribe((c, a) => { + this.active_column = c; + this.ascending = a; + }); } render(): TemplateResult { From a72a05952fb16f19c770691267f0174a8503efaa Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 31 Mar 2022 09:11:33 +0200 Subject: [PATCH 08/73] Fix searchfilter options --- .../javascripts/components/search_actions.ts | 67 ++++++++++++------- .../javascripts/components/sort_button.ts | 1 - app/assets/javascripts/index.js | 4 +- app/assets/javascripts/search.ts | 42 +++++++++++- app/javascript/packs/application_pack.js | 1 + app/views/layouts/_searchbar.html.erb | 17 +++-- 6 files changed, 96 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/components/search_actions.ts b/app/assets/javascripts/components/search_actions.ts index 33616bb404..de6d7375a3 100644 --- a/app/assets/javascripts/components/search_actions.ts +++ b/app/assets/javascripts/components/search_actions.ts @@ -14,13 +14,16 @@ type SearchAction = { icon: string }; +const isSearchOption = (opt): opt is SearchOption => (opt as SearchOption).search !== undefined; +const isSearchAction= (action): action is SearchAction => (action as SearchAction).js !== undefined || (action as SearchAction).action !== undefined || (action as SearchAction).url !== undefined; + @customElement("dodona-search-option") export class SearchOptionElement extends LitElement { - @property() - searchOption: SearchOption; - @property() - key: number; - @state() + @property({ type: Object }) + searchOption: SearchOption; + @property( { type: Number }) + key: number; + private _active = false; // don't use shadow dom @@ -28,23 +31,33 @@ export class SearchOptionElement extends LitElement { return this; } - constructor() { - super(); - Object.keys(this.searchOption.search).forEach(k => { - dodona.search_query.query_params.subscribeByKey(k, () => this.setActive()); - }); + update(changedProperties: Map): void { + if (changedProperties.has("searchOption") && this.searchOption) { + console.log(this.searchOption); + this.setActive(); + Object.keys(this.searchOption.search).forEach(k => { + dodona.search_query.query_params.subscribeByKey(k, () => this.setActive()); + }); + } + super.update(changedProperties); } setActive(): void { this._active = Object.entries(this.searchOption.search).every(([key, value]) => { - return dodona.search_query.query_params.params.get(key) == value; + return dodona.search_query.query_params.params.get(key) == value.toString(); }); } performSearch(): void { - Object.entries(this.searchOption.search).forEach(([key, value]) => { - dodona.search_query.query_params.updateParam(key, value); - }); + if (!this._active) { + Object.entries(this.searchOption.search).forEach(([key, value]) => { + dodona.search_query.query_params.updateParam(key, value.toString()); + }); + } else { + Object.keys(this.searchOption.search).forEach(key => { + dodona.search_query.query_params.updateParam(key, undefined); + }); + } } render(): TemplateResult { @@ -69,10 +82,16 @@ export class SearchOptionElement extends LitElement { @customElement("dodona-search-actions") export class SearchActions extends LitElement { - @property() - searchOptions: Array; - @property() - searchActions: Array; + @property({ type: Array }) + actions: (SearchOption|SearchAction)[] = []; + + getSearchOptions(): Array { + return this.actions.filter(isSearchOption); + } + + getSearchActions(): Array { + return this.actions.filter(isSearchAction); + } // don't use shadow dom createRenderRoot(): Element { @@ -111,23 +130,23 @@ export class SearchActions extends LitElement { render(): TemplateResult { return html` - @@ -183,5 +185,6 @@ filterCollections, "<%= local_assigns.fetch :refresh_element, "" %>" ); + dodona.search_query.setRefreshElement("<%= local_assigns.fetch :refresh_element, "" %>"); }); From cb0887329349eb87612399e52047f6cbaef0ed95 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 31 Mar 2022 10:04:11 +0200 Subject: [PATCH 09/73] Create search field component --- .../javascripts/components/search_field.ts | 52 +++++++++++++++++++ app/javascript/packs/application_pack.js | 1 + app/views/layouts/_searchbar.html.erb | 1 + 3 files changed, 54 insertions(+) create mode 100644 app/assets/javascripts/components/search_field.ts diff --git a/app/assets/javascripts/components/search_field.ts b/app/assets/javascripts/components/search_field.ts new file mode 100644 index 0000000000..cb0e90b72a --- /dev/null +++ b/app/assets/javascripts/components/search_field.ts @@ -0,0 +1,52 @@ +import { customElement, property } from "lit/decorators.js"; +import { html, LitElement, TemplateResult } from "lit"; +import { createDelayer } from "util.js"; + +@customElement("dodona-search-field") +export class SearchField extends LitElement { + @property({ type: String }) + placeholder: string; + @property({ type: Boolean }) + eager: boolean; + + filter?: string = ""; + delay: (f: () => void, s: number) => void; + + // don't use shadow dom + createRenderRoot(): Element { + return this; + } + + constructor() { + super(); + dodona.search_query.query_params.subscribeByKey("filter", (k, o, n) => this.filter = n || ""); + this.filter = dodona.search_query.query_params.params.get("filter") || ""; + this.delay = createDelayer(); + } + + update(changedProperties: Map): void { + if (changedProperties.has("eager") && this.eager) { + dodona.search_query.search(); + } + super.update(changedProperties); + } + + keyup(e: KeyboardEvent): void { + this.filter = (e.target as HTMLInputElement).value; + this.delay(() => dodona.search_query.query_params.updateParam("filter", this.filter), 300); + } + + render(): TemplateResult { + return html` + this.keyup(e)} + /> + `; + } +} diff --git a/app/javascript/packs/application_pack.js b/app/javascript/packs/application_pack.js index 890eb113bf..dfd83dcae7 100644 --- a/app/javascript/packs/application_pack.js +++ b/app/javascript/packs/application_pack.js @@ -30,6 +30,7 @@ import "polyfills.js"; import "components/sort_button.ts"; import "components/search_actions.ts"; +import "components/search_field.ts"; import { Drawer } from "drawer"; import { Toast } from "toast"; diff --git a/app/views/layouts/_searchbar.html.erb b/app/views/layouts/_searchbar.html.erb index 71db6073f3..30fa250d9f 100644 --- a/app/views/layouts/_searchbar.html.erb +++ b/app/views/layouts/_searchbar.html.erb @@ -6,6 +6,7 @@