Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
27 changes: 18 additions & 9 deletions app/views/timer_sessions/_timer_container.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<div class="box" data-controller="form timer" data-timer-timezone-value="<%= offset_for_time_zone %>">
<div class="box" data-controller="form timer" data-timer-timezone-value="<%= offset_for_time_zone %>"
data-form-share-copied-value="<%= t('timer_sessions.timer.share_copied') %>"
data-form-share-ignored-value="<%= t('timer_sessions.timer.share_ignored') %>"
data-form-session-active-value="<%= timer_session.persisted? %>">
<div class="timer-container">
<% active_timer_session = timer_session %>
<%= labelled_form_for(active_timer_session,
Expand Down Expand Up @@ -80,14 +83,20 @@
<% end %>
</div>
<% elsif !timer_session.persisted? && User.current.allowed_to_globally?(action: :create, controller: 'time_tracker') %>
<%= f.button :start, type: :submit, value: :start, data: { name: 'timer-start' }, name: :commit do %>
<%= t('timer_sessions.timer.start') %>
<%= sprite_icon('add') %>
<% end %>
<%= f.button :start, type: :submit, value: :continue_last_session, data: { name: 'timer-continue' }, class: 'ml-3', name: :commit do %>
<%= t('timer_sessions.timer.continue_last_session') %>
<%= sprite_icon('add') %>
<% end %>
<div class="starting-action-buttons">
<%= f.button :start, type: :submit, value: :start, data: { name: 'timer-start' }, name: :commit do %>
<%= t('timer_sessions.timer.start') %>
<%= sprite_icon('add') %>
<% end %>
<%= f.button :start, type: :submit, value: :continue_last_session, data: { name: 'timer-continue' }, name: :commit do %>
<%= t('timer_sessions.timer.continue_last_session') %>
<%= sprite_icon('add') %>
<% end %>
<button type="button" data-name="timer-share" data-action="click->form#share" class="btn-submit">
<%= t('timer_sessions.timer.share') %>
<%= sprite_icon('link') %>
</button>
</div>
<% end %>
<% end %>

Expand Down
83 changes: 83 additions & 0 deletions assets.src/src/redmine-tracky/controllers/form-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export default class extends Controller {
declare readonly absolutInputTarget: HTMLInputElement
declare readonly descriptionTarget: HTMLInputElement
declare readonly issueTargets: Element[]
declare readonly shareCopiedValue: string
declare readonly shareIgnoredValue: string
declare readonly sessionActiveValue: boolean

private connected = false

Expand All @@ -21,8 +24,15 @@ export default class extends Controller {
'absolutInput',
]

static values = {
shareCopied: String,
shareIgnored: String,
sessionActive: Boolean,
}

public connect() {
this.connected = true
this.showShareIgnoredNotice()
}

public disconnect() {
Expand Down Expand Up @@ -67,6 +77,79 @@ export default class extends Controller {
this.dispatchUpdate(form)
}

public share(_event: Event) {
const parts: string[] = []
const issueIds = this.extractIssueIds()
const comments = this.descriptionTarget.value
const timerStart = this.startTarget.value
const timerEnd = this.endTarget.value

issueIds.forEach((id) => parts.push(`issue_ids[]=${encodeURIComponent(id)}`))
if (comments) parts.push(`comments=${encodeURIComponent(comments)}`)
if (timerStart) parts.push(`timer_start=${encodeURIComponent(timerStart)}`)
if (timerEnd) parts.push(`timer_end=${encodeURIComponent(timerEnd)}`)

const query = parts.length > 0 ? `?${parts.join('&')}` : ''
const url = `${window.location.origin}${window.location.pathname}${query}`

this.copyToClipboard(url).then(() => {
this.showFlashNotice(this.shareCopiedValue)
})
}

private copyToClipboard(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
return navigator.clipboard.writeText(text).catch(() => {
this.copyToClipboardFallback(text)
})
}
this.copyToClipboardFallback(text)
return Promise.resolve()
}

private copyToClipboardFallback(text: string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm

const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}

private showShareIgnoredNotice() {
if (!this.sessionActiveValue) return

const urlParams = new URLSearchParams(window.location.search)
const hasShareParams = urlParams.has('comments') ||
urlParams.has('timer_start') ||
urlParams.has('timer_end') ||
urlParams.getAll('issue_ids[]').some((v) => v !== '')

if (hasShareParams) {
this.showFlashNotice(this.shareIgnoredValue)
}
}

private showFlashNotice(message: string) {
const flash = document.getElementById('flash_notice')
if (flash) {
flash.textContent = message
flash.style.display = ''
return
}

const container = document.getElementById('content')
if (!container) return

const notice = document.createElement('div')
notice.id = 'flash_notice'
notice.className = 'flash notice'
notice.textContent = message
container.prepend(notice)
}

private extractIssueIds(): string[] {
return (
this.issueTargets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default class extends Controller {

connect() {
this.listenForInput()
this.fetchIssuesFromURL()
this.prefillFromURL()
}

private listenForInput() {
Expand All @@ -43,8 +43,16 @@ export default class extends Controller {
)
}

private fetchIssuesFromURL() {
private prefillFromURL() {
const urlParams = new URLSearchParams(window.location.search)

this.prefillIssuesFromURL(urlParams)
this.prefillFieldFromURL(urlParams, 'comments', '#timer_session_comments')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use targets instead of #

this.prefillFieldFromURL(urlParams, 'timer_start', '#timer_session_timer_start')
this.prefillFieldFromURL(urlParams, 'timer_end', '#timer_session_timer_end')
}

private prefillIssuesFromURL(urlParams: URLSearchParams) {
const issueIds = urlParams.getAll('issue_ids[]')

issueIds.filter(v => v !== "").forEach((id) => {
Expand All @@ -62,6 +70,17 @@ export default class extends Controller {
})
}

private prefillFieldFromURL(urlParams: URLSearchParams, param: string, selector: string) {
const value = urlParams.get(param)
if (!value) return

const field = document.querySelector<HTMLInputElement>(selector)
if (field) {
field.value = value
field.dispatchEvent(new Event('change'))
}
}

private addIssue(issue: { item: CompletionResult }) {
const listController =
this.application.getControllerForElementAndIdentifier(
Expand Down
20 changes: 20 additions & 0 deletions assets.src/src/styles/timer_container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
column-count: 2;
}

.starting-action-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}

.btn-submit {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm
i'd expect here a ref to [submit]

-webkit-appearance: button;
cursor: pointer;
background-color: #fff;
height: 28px;
border: 1px solid #ccc;
padding: 0 7px;
transition: background-color 100ms linear;

&:hover {
background-color: #ededed;
}
}

.timer-container button svg {
stroke: #fff;
}
Expand Down
2 changes: 1 addition & 1 deletion assets/javascripts/redmine-tracky.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ de:
timer:
start: Start
continue_last_session: Anschliessend Starten
share: Teilen
share_copied: Link in die Zwischenablage kopiert
share_ignored: Ein Timer läuft bereits. Die geteilten Parameter wurden ignoriert.
stop: Stop
cancel: Abbrechen
date_placeholder: 'dd.mm.yyyy hh:mm'
Expand Down
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ en:
timer:
start: Start
continue_last_session: Start on end of last session
share: Share
share_copied: Link copied to clipboard
share_ignored: A timer is already running. Shared session parameters were ignored.
stop: Stop
cancel: Cancel
date_placeholder: 'dd.mm.yyyy hh:mm'
Expand Down
56 changes: 56 additions & 0 deletions test/system/timer_management_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,62 @@ class TimerManagementTest < ApplicationSystemTestCase
assert_no_text Issue.second.subject
end

test 'loading timer with comments from url' do
visit timer_sessions_path(comments: 'Meeting with team')

assert_equal 'Meeting with team', find('#timer_session_comments').value
end

test 'loading timer with timer_start from url' do
visit timer_sessions_path(timer_start: '01.01.2026 09:00')

assert_equal '01.01.2026 09:00', find('#timer_session_timer_start').value
end

test 'loading timer with timer_end from url' do
visit timer_sessions_path(timer_end: '01.01.2026 17:00')

assert_equal '01.01.2026 17:00', find('#timer_session_timer_end').value
end

test 'loading timer with all query params from url' do
visit timer_sessions_path(
issue_ids: [Issue.first.id],
comments: 'Sprint planning',
timer_start: '01.01.2026 09:00',
timer_end: '01.01.2026 10:00'
)

assert_text Issue.first.subject
assert_equal 'Sprint planning', find('#timer_session_comments').value
assert_equal '01.01.2026 09:00', find('#timer_session_timer_start').value
assert_equal '01.01.2026 10:00', find('#timer_session_timer_end').value
end

test 'share button is visible and clickable' do
visit timer_sessions_path

fill_in 'timer_session_comments', with: 'Pairing session'

find('[data-name="timer-share"]', wait: 5).click
assert_text I18n.t('timer_sessions.timer.share_copied')
end

test 'share button not visible when timer is active' do
FactoryBot.create(:timer_session, finished: false, user: User.current)
visit timer_sessions_path

assert has_content?(I18n.t('timer_sessions.timer.stop'))
assert_no_selector('[data-name="timer-share"]')
end

test 'shows prefill notice when active session exists and url has params' do
FactoryBot.create(:timer_session, finished: false, user: User.current)
visit timer_sessions_path(comments: 'Meeting', issue_ids: [Issue.first.id])

assert_text I18n.t('timer_sessions.timer.share_ignored')
end

test 'preserves filter parameters when stopping a timer' do
filter_date = 1.week.ago.strftime('%Y-%m-%d')
current_date = Date.today.strftime('%Y-%m-%d')
Expand Down