Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/javascript/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import "@controllers/invite_form_controller"
import "@controllers/invite_picker_controller"
import "@controllers/item_form_controller"
import "@controllers/join_code_controller"
import "@controllers/leave_guard_controller"
import "@controllers/list_search_controller"
import "@controllers/lives_stepper_controller"
import "@controllers/menu_controller"
Expand Down
88 changes: 88 additions & 0 deletions app/javascript/controllers/leave_guard_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { application } from "@controllers/application"
import { Controller } from "@hotwired/stimulus"

type VisitEvent = CustomEvent<{ url: string }>

class LeaveGuardController extends Controller<HTMLElement> {
static targets = ["form", "dialog"]

declare readonly formTarget: HTMLFormElement
declare readonly hasFormTarget: boolean
declare readonly dialogTarget: HTMLDialogElement

private bypass = false
private pendingUrl: string | null = null

connect() {
window.addEventListener("turbo:before-visit", this.onBeforeVisit)
window.addEventListener("beforeunload", this.onBeforeUnload)
}

disconnect() {
window.removeEventListener("turbo:before-visit", this.onBeforeVisit)
window.removeEventListener("beforeunload", this.onBeforeUnload)
}

// Submitting the form saves the changes, so leaving afterwards is fine.
submitting() {
this.bypass = true
}

stay() {
this.pendingUrl = null
this.dialogTarget.close()
}

leave() {
const url = this.pendingUrl

this.bypass = true
this.pendingUrl = null
this.dialogTarget.close()

if (url) Turbo.visit(url)
}

closeOnBackdrop(event: MouseEvent) {
if (event.target !== event.currentTarget) return

this.stay()
}

private onBeforeVisit = (event: Event) => {
if (!this.guarding) return

event.preventDefault()
this.pendingUrl = (event as VisitEvent).detail.url

if (!this.dialogTarget.open) this.dialogTarget.showModal()
}

private onBeforeUnload = (event: BeforeUnloadEvent) => {
if (!this.guarding) return

event.preventDefault()
}

private get guarding(): boolean {
return !this.bypass && this.dirty
}

private get dirty(): boolean {
if (!this.hasFormTarget) return false

return Array.from(this.formTarget.elements).some((field) => {
if (field instanceof HTMLInputElement && ["checkbox", "radio"].includes(field.type)) {
return field.checked !== field.defaultChecked
}

if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) {
return field.type !== "hidden" && field.value !== field.defaultValue
}

return false
})
}
}

application.register("leave-guard", LeaveGuardController)
2 changes: 1 addition & 1 deletion app/models/badge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Badge < ApplicationRecord

validates :discount, numericality: {
greater_than_or_equal_to: 0,
less_than_or_equal_to: 100,
less_than_or_equal_to: Discount::CAP_VALUE,
message: :out_of_range,
}

Expand Down
2 changes: 1 addition & 1 deletion app/models/rank.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Rank < ApplicationRecord

validates :discount, numericality: {
greater_than_or_equal_to: 0,
less_than_or_equal_to: 100,
less_than_or_equal_to: Discount::CAP_VALUE,
message: :out_of_range,
}

Expand Down
2 changes: 1 addition & 1 deletion app/models/students_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ class StudentsItem < ApplicationRecord
belongs_to :story_group_student
belongs_to :item

validates :discount_applied, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 50 }
validates :discount_applied, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Discount::CAP_VALUE }
validates :price_paid, presence: true, numericality: { greater_than_or_equal_to: 0 }
end
4 changes: 2 additions & 2 deletions app/values/item_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def discount_badges
end

def ladder_discount
discounts = ranks.filter_map { |rank| rank.discount&.clamp(0, 100) }
discounts = ranks.filter_map { |rank| rank.discount&.clamp(0, Discount::CAP_VALUE) }
discounts.max.to_i
end

Expand Down Expand Up @@ -97,7 +97,7 @@ def badge_requirements
end

def rank_discount
discounts = eligible_ranks.filter_map { |rank| rank.discount&.clamp(0, 100) }
discounts = eligible_ranks.filter_map { |rank| rank.discount&.clamp(0, Discount::CAP_VALUE) }
discounts.max.to_i
end

Expand Down
2 changes: 1 addition & 1 deletion app/views/badges/_form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
= f.label :discount, t('.discount')
.gh-price-field-row
.gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error.call(:discount)) }
= f.number_field :discount, min: 0, max: 100, step: 1,
= f.number_field :discount, min: 0, max: Discount::CAP_VALUE, step: 1,
'aria-invalid': (error.call(:discount) ? 'true' : nil),
'aria-describedby': (error.call(:discount) ? 'gh-badge-disc-error' : nil),
data: { badge_form_target: 'discount', action: 'badge-form#refresh' }
Expand Down
2 changes: 1 addition & 1 deletion app/views/ranks/_form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
= f.label :discount, t('.discount')
.gh-price-field-row
.gh-text-input.gh-text-input--numeric{ class: ('gh-text-input--invalid' if error.call(:discount)) }
= f.number_field :discount, min: 0, max: 100, step: 1,
= f.number_field :discount, min: 0, max: Discount::CAP_VALUE, step: 1,
'aria-invalid': (error.call(:discount) ? 'true' : nil),
'aria-describedby': (error.call(:discount) ? 'gh-rank-disc-error' : nil),
data: { rank_form_target: 'discount', action: 'rank-form#refresh' }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
%dialog.gh-dialog{
'aria-labelledby': 'gh-leave-title',
data: {
leave_guard_target: 'dialog',
action: 'close->leave-guard#stay click->leave-guard#closeOnBackdrop',
},
}
.gh-dialog-body.gh-octagon-clip
%h2.gh-h2#gh-leave-title= t('.title')

%p.gh-warning-note{ role: 'alert' }
%i.fa-solid.fa-triangle-exclamation{ 'aria-hidden': 'true' }
= t('.body')

.gh-dialog-button-row
%button.gh-btn.gh-btn--secondary{type: 'button', autofocus: true,
data: { action: 'leave-guard#stay' },}
= t('.stay')
%button.gh-btn{ type: 'button', data: { action: 'leave-guard#leave' } }
= t('.leave')
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
%dialog.gh-dialog.gh-dialog--grading-review{ 'aria-labelledby': 'gh-review-title',
data: { grading_target: 'dialog',
action: 'close->grading#closeReview click->grading#closeOnBackdrop' } }
%dialog.gh-dialog.gh-dialog--grading-review{
'aria-labelledby': 'gh-review-title',
data: { grading_target: 'dialog',
action: 'close->grading#closeReview click->grading#closeOnBackdrop', },
}
.gh-dialog-body.gh-octagon-clip
%h2#gh-review-title.gh-h2
%h2.gh-h2#gh-review-title
= t('.awarding')
%span{ data: { grading_target: 'reviewSum' } }
= @story_group.currency_name
= story_group.currency_name
%span{ data: { grading_target: 'reviewStudents' } }

%p.gh-warning-note{ role: 'alert' }
Expand All @@ -14,28 +16,31 @@

%ul.gh-review-list
- students.each do |student|
- name = student.full_name
- open_categories = categories.reject { |category| @sheet.awarded?(student, category) }
- next if open_categories.empty?
:ruby
name = student.full_name
open_categories = categories.reject { |category| sheet.awarded?(student, category) }
next if open_categories.empty?

%li{ hidden: true, data: { grading_target: 'reviewRow', gh_student: student.id } }
%span.gh-review-row-name
%span.gh-avatar.gh-avatar--sm.gh-octagon-clip{ 'data-gh-h': gh_avatar_hue(name), 'aria-hidden': 'true' }
= gh_initials(name)
= name
%span.gh-review-row-chips
- open_categories.each do |category|
%span.gh-review-chip.gh-octagon-clip{ hidden: true,
data: { grading_target: 'reviewChip',
gh_student: student.id, gh_column: category.id } }
%span.gh-review-chip.gh-octagon-clip{hidden: true,
data: { grading_target: 'reviewChip',
gh_student: student.id,
gh_column: category.id, },}
= category.didactic_description
%b= "+#{category.reward.to_i}"
%span.gh-review-row-sum{ data: { grading_target: 'reviewRowSum' } }

.gh-dialog-button-row
%button.gh-btn.gh-btn--secondary{ type: 'button', autofocus: true,
data: { action: 'grading#closeReview' } }
%button.gh-btn.gh-btn--secondary{type: 'button', autofocus: true,
data: { action: 'grading#closeReview' },}
= t('shared.actions.back_to_editing')
%button.gh-btn.gh-btn--gold{ type: 'submit', form: 'gh-grade-form' }
= t('.confirm')
%span{ data: { grading_target: 'reviewConfirmSum' } }
= @story_group.currency_name
= story_group.currency_name
Loading
Loading