Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85828533
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
11 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Oct 31, 8:09 AM (17 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801178
Default Alt Text
(11 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline