Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
11 KB
Referenced Files
None
Subscribers
None
diff --git a/changelog.d/poll-expiry-unit.fix b/changelog.d/poll-expiry-unit.fix
new file mode 100644
index 0000000000..b99c04f1d2
--- /dev/null
+++ b/changelog.d/poll-expiry-unit.fix
@@ -0,0 +1 @@
+Polls set to hours or days no longer end after minutes: the unit went back to minutes while the field still showed the chosen one.
diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js
index a9c9151271..c3513ae325 100644
--- a/src/components/poll/poll_form.js
+++ b/src/components/poll/poll_form.js
@@ -1,164 +1,166 @@
import Select from 'src/components/select/select.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import * as DateUtils from 'src/services/date_utils/date_utils.js'
import { pollFallback } from 'src/services/poll/poll.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
library.add(faTimes, faPlus)
export default {
components: {
Select,
},
name: 'PollForm',
props: {
visible: Boolean,
modelValue: {
type: Object,
required: false,
default: null,
},
},
emits: ['update:modelValue'],
computed: {
// Model value
options() {
return pollFallback(this.modelValue, 'options')
},
pollType: {
get() {
return pollFallback(this.modelValue, 'pollType')
},
set(newVal) {
this.$emit('update:modelValue', {
...this.modelValue,
pollType: newVal,
})
},
},
expiryAmount: {
get() {
return pollFallback(this.modelValue, 'expiryAmount')
},
set(newVal) {
this.$emit('update:modelValue', {
...this.modelValue,
expiryAmount: newVal,
})
},
},
expiryUnit: {
get() {
return pollFallback(this.modelValue, 'expiryUnit')
},
+ // the unit and the amount it allows in one update: a second one in
+ // the same tick would start from the old unit (the prop catches up
+ // only on the parent's next render)
set(newVal) {
this.$emit('update:modelValue', {
...this.modelValue,
expiryUnit: newVal,
+ expiryAmount: this.clampExpiry(newVal, this.expiryAmount),
})
},
},
// Configuration
pollLimits() {
return useInstanceStore().limits.pollLimits
},
maxOptions() {
return this.pollLimits.max_options
},
maxLength() {
return this.pollLimits.max_option_chars
},
expiryUnits() {
const allUnits = ['minutes', 'hours', 'days']
const expiry = this.convertExpiryFromUnit
return allUnits.filter(
(unit) => this.pollLimits.max_expiration >= expiry(unit, 1),
)
},
minExpirationInCurrentUnit() {
- return Math.ceil(
- this.convertExpiryToUnit(
- this.expiryUnit,
- this.pollLimits.min_expiration,
- ),
- )
+ return this.expiryLimits(this.expiryUnit).min
},
maxExpirationInCurrentUnit() {
- return Math.floor(
- this.convertExpiryToUnit(
- this.expiryUnit,
- this.pollLimits.max_expiration,
- ),
- )
+ return this.expiryLimits(this.expiryUnit).max
},
},
methods: {
nextOption(index) {
const element = this.$el.querySelector(`#poll-${index + 1}`)
if (element) {
element.focus()
} else {
// Try adding an option and try focusing on it
const addedOption = this.addOption()
if (addedOption) {
this.$nextTick(function () {
this.nextOption(index)
})
}
}
},
addOption() {
if (this.options.length < this.maxOptions) {
this.$emit('update:modelValue', {
...this.modelValue,
options: [...this.options, ''],
})
return true
}
return false
},
deleteOption(index) {
if (this.options.length > 2) {
const options = [...this.options]
options.splice(index, 1)
this.$emit('update:modelValue', {
...this.modelValue,
options,
})
}
},
updateOption(index, value) {
const options = [...this.options]
options[index] = value
this.$emit('update:modelValue', {
...this.modelValue,
options,
})
},
convertExpiryToUnit(unit, amount) {
// Note: we want seconds and not milliseconds
return DateUtils.secondsToUnit(unit, amount)
},
convertExpiryFromUnit(unit, amount) {
return DateUtils.unitToSeconds(unit, amount)
},
- expiryAmountChange() {
- this.expiryAmount = Math.max(
- this.minExpirationInCurrentUnit,
- this.expiryAmount,
- )
- this.expiryAmount = Math.min(
- this.maxExpirationInCurrentUnit,
- this.expiryAmount,
- )
+ expiryLimits(unit) {
+ return {
+ min: Math.ceil(
+ this.convertExpiryToUnit(unit, this.pollLimits.min_expiration),
+ ),
+ max: Math.floor(
+ this.convertExpiryToUnit(unit, this.pollLimits.max_expiration),
+ ),
+ }
+ },
+ clampExpiry(unit, amount) {
+ const { min, max } = this.expiryLimits(unit)
+ return Math.min(max, Math.max(min, Number(amount)))
+ },
+ // from the field itself: the prop can still hold the value before
+ expiryAmountChange(event) {
+ this.expiryAmount = this.clampExpiry(this.expiryUnit, event.target.value)
},
},
}
diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue
index 36194995a2..fa4a87b666 100644
--- a/src/components/poll/poll_form.vue
+++ b/src/components/poll/poll_form.vue
@@ -1,159 +1,158 @@
<template>
<div
v-if="visible"
class="poll-form"
>
<div
v-for="(option, index) in options"
:key="index"
class="poll-option"
>
<div class="input-container">
<input
:id="`poll-${index}`"
:model-value="options[index]"
size="1"
class="input poll-option-input"
type="text"
:placeholder="$t('polls.option')"
:maxlength="maxLength"
:aria-label="$t('polls.option')"
@keydown.enter.stop.prevent="nextOption(index)"
@change="(e) => updateOption(index, e.target.value)"
>
</div>
<button
v-if="options.length > 2"
class="delete-option button-unstyled -hover-highlight"
@click="deleteOption(index)"
>
<FAIcon icon="times" />
</button>
</div>
<button
v-if="options.length < maxOptions"
class="add-option faint button-unstyled -hover-highlight"
@click="addOption"
>
<FAIcon
icon="plus"
size="sm"
/>
{{ $t("polls.add_option") }}
</button>
<div class="poll-type-expiry">
<div
class="poll-type"
>
<Select
v-model="pollType"
class="poll-type-select"
:title="$t('polls.type')"
unstyled
>
<option value="single">
{{ $t('polls.single_choice') }}
</option>
<option value="multiple">
{{ $t('polls.multiple_choices') }}
</option>
</Select>
</div>
<div
class="poll-expiry"
:title="$t('polls.expiry')"
>
<input
v-model="expiryAmount"
type="number"
class="input expiry-amount hide-number-spinner"
:min="minExpirationInCurrentUnit"
:max="maxExpirationInCurrentUnit"
:aria-label="$t('polls.expiry_amount')"
@change="expiryAmountChange"
>
{{ ' ' }}
<Select
v-model="expiryUnit"
unstyled
class="expiry-unit"
:aria-label="$t('polls.expiry_unit')"
- @change="expiryAmountChange"
>
<option
v-for="unit in expiryUnits"
:key="unit"
:value="unit"
>
{{ $t(`time.unit.${unit}_short`, [''], expiryAmount) }}
</option>
</Select>
</div>
</div>
</div>
</template>
<script src="./poll_form.js"></script>
<style lang="scss">
.poll-form {
display: flex;
flex-direction: column;
padding: 0 0.5em 0.5em;
.add-option {
align-self: flex-start;
padding-top: 0.25em;
padding-left: 0.1em;
}
.poll-option {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.25em;
}
.input-container {
width: 100%;
input {
// Hack: dodge the floating X icon
padding-right: 2.5em;
width: 100%;
}
}
.delete-option {
// Hack: Move the icon over the input box
width: 1.5em;
margin-left: -1.5em;
z-index: 1;
}
.poll-type-expiry {
margin-top: 0.5em;
display: flex;
width: 100%;
}
.poll-type {
margin-right: 0.75em;
flex: 1 1 60%;
.poll-type-select {
padding-right: 0.75em;
}
}
.poll-expiry {
display: flex;
.expiry-amount {
width: 3em;
text-align: right;
}
}
}
</style>
diff --git a/test/unit/specs/components/poll_form.spec.js b/test/unit/specs/components/poll_form.spec.js
new file mode 100644
index 0000000000..0eb6665bc2
--- /dev/null
+++ b/test/unit/specs/components/poll_form.spec.js
@@ -0,0 +1,47 @@
+import { createTestingPinia } from '@pinia/testing'
+import { mount } from '@vue/test-utils'
+import { mountOpts } from 'test/fixtures/setup_test.js'
+
+import PollForm from 'src/components/poll/poll_form.vue'
+
+import { useInstanceStore } from 'src/stores/instance.js'
+
+import { pollFormToMasto } from 'src/services/poll/poll.service.js'
+
+// The form inside a parent that keeps the poll, as the post form does: the
+// prop only catches up on the parent's next render.
+const Parent = {
+ components: { PollForm },
+ data: () => ({ poll: {} }),
+ template: '<PollForm v-model="poll" :visible="true" />',
+}
+
+const make = () => {
+ const pinia = createTestingPinia({ stubActions: false })
+ useInstanceStore().limits.pollLimits = {
+ max_options: 4,
+ max_option_chars: 50,
+ min_expiration: 60,
+ max_expiration: 30 * 86400,
+ }
+ return mount(Parent, mountOpts({ global: { plugins: [pinia] } }))
+}
+
+describe('PollForm expiry', () => {
+ it('keeps a chosen unit of days (it went back to minutes)', async () => {
+ const wrapper = make()
+ await wrapper.get('.expiry-unit select').setValue('days')
+ await wrapper.get('.expiry-amount').setValue('1')
+ expect(wrapper.vm.poll).to.include({ expiryUnit: 'days' })
+ expect(Number(wrapper.vm.poll.expiryAmount)).to.equal(1)
+ wrapper.vm.poll.options = ['a', 'b']
+ expect(pollFormToMasto(wrapper.vm.poll).expiresIn).to.equal(86400)
+ })
+
+ it('keeps the amount within the limits in the chosen unit', async () => {
+ const wrapper = make()
+ await wrapper.get('.expiry-unit select').setValue('days')
+ await wrapper.get('.expiry-amount').setValue('90')
+ expect(wrapper.vm.poll).to.include({ expiryUnit: 'days', expiryAmount: 30 })
+ })
+})

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 31, 8:09 AM (18 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801178
Default Alt Text
(11 KB)

Event Timeline