From a04e286301acb5133abe3146b868920299831076 Mon Sep 17 00:00:00 2001
From: philippe lhardy
Date: Wed, 30 Jul 2025 08:40:38 +0200
Subject: [PATCH] feat: add rank voting feature merge chosenrank with 8.1.4
refactorings
reworked from
git@github.com:vinimoz/polls.git rank-vote-feature-vue3
TRAPPE Vincent
---
lib/Db/Poll.php | 18 +++
lib/Migration/TableSchema.php | 1 +
lib/ResponseDefinitions.php | 1 +
lib/Service/OptionService.php | 2 +-
lib/Service/PollService.php | 4 +-
.../Cards/modules/CardAddProposals.vue | 2 +-
.../Configuration/ConfigRankOptions.vue | 114 ++++++++++++++++++
.../Navigation/PollNavigationItems.vue | 2 +-
src/components/Options/OptionItem.vue | 2 +-
src/components/PollList/PollItem.vue | 4 +
.../SideBar/SideBarTabConfiguration.vue | 13 +-
src/components/SideBar/SideBarTabOptions.vue | 2 +-
src/components/User/UserMenu.vue | 4 +
src/components/VoteTable/VoteIndicator.vue | 93 ++++++++++++--
src/components/VoteTable/VoteItem.vue | 23 +++-
src/components/VoteTable/VoteTable.vue | 39 +++++-
src/stores/poll.ts | 35 +++++-
src/views/Dashboard.vue | 1 +
src/views/Vote.vue | 1 +
19 files changed, 339 insertions(+), 22 deletions(-)
create mode 100755 src/components/Configuration/ConfigRankOptions.vue
diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php
index f5f8be30a1..51e8978747 100644
--- a/lib/Db/Poll.php
+++ b/lib/Db/Poll.php
@@ -41,6 +41,8 @@
* @method void setAllowComment(int $value)
* @method int getAllowMaybe()
* @method void setAllowMaybe(int $value)
+ * @method string getChosenRank()
+ * @method void setChosenRank(string $value)
* @method string getAllowProposals()
* @method void setAllowProposals(string $value)
* @method int getProposalsExpire()
@@ -79,6 +81,7 @@
class Poll extends EntityWithUser implements JsonSerializable {
public const TABLE = 'polls_polls';
public const TYPE_DATE = 'datePoll';
+ public const TYPE_GENERIC = 'genericPoll';
public const TYPE_TEXT = 'textPoll';
public const VARIANT_SIMPLE = 'simple';
public const ACCESS_HIDDEN = 'hidden';
@@ -146,6 +149,7 @@ class Poll extends EntityWithUser implements JsonSerializable {
protected string $access = '';
protected int $anonymous = 0;
protected int $allowMaybe = 0;
+ protected string $chosenRank = '';
protected string $allowProposals = '';
protected int $proposalsExpire = 0;
protected int $voteLimit = 0;
@@ -181,6 +185,7 @@ public function __construct() {
$this->addType('anonymous', 'integer');
$this->addType('allowComment', 'integer');
$this->addType('allowMaybe', 'integer');
+ $this->addType('chosenRank', 'string');
$this->addType('proposalsExpire', 'integer');
$this->addType('voteLimit', 'integer');
$this->addType('optionLimit', 'integer');
@@ -246,6 +251,7 @@ public function getConfigurationArray(): array {
'access' => $this->getAccess(),
'allowComment' => boolval($this->getAllowComment()),
'allowMaybe' => boolval($this->getAllowMaybe()),
+ 'chosenRank' => $this->getChosenRank(),
'allowProposals' => $this->getAllowProposals(),
'anonymous' => boolval($this->getAnonymous()),
'autoReminder' => $this->getAutoReminder(),
@@ -315,6 +321,18 @@ public function deserializeArray(array $pollConfiguration): self {
$this->setAccess($pollConfiguration['access'] ?? $this->getAccess());
$this->setAllowComment($pollConfiguration['allowComment'] ?? $this->getAllowComment());
$this->setAllowMaybe($pollConfiguration['allowMaybe'] ?? $this->getAllowMaybe());
+ $chosenRank = $pollConfiguration['chosenRank'] ?? $this->getChosenRank();
+ if (is_array($chosenRank)) {
+ $chosenRank = json_encode($chosenRank); // Sérialisation explicite
+ } elseif (is_string($chosenRank)) {
+ if (!json_decode($chosenRank)) {
+ $chosenRank = '[]'; // Fallback si JSON invalide
+ }
+ } else {
+ $chosenRank = '[]'; // Fallback
+ }
+
+ $this->setChosenRank($chosenRank);
$this->setAllowProposals($pollConfiguration['allowProposals'] ?? $this->getAllowProposals());
$this->setAnonymousSafe($pollConfiguration['anonymous'] ?? $this->getAnonymous());
$this->setAutoReminder($pollConfiguration['autoReminder'] ?? $this->getAutoReminder());
diff --git a/lib/Migration/TableSchema.php b/lib/Migration/TableSchema.php
index 53954cd266..f0d79f6ebb 100644
--- a/lib/Migration/TableSchema.php
+++ b/lib/Migration/TableSchema.php
@@ -195,6 +195,7 @@ abstract class TableSchema {
'access' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => 'private', 'length' => 1024]],
'anonymous' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
'allow_maybe' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 1, 'length' => 20]],
+ 'chosen_rank' => ['type' => Types::TEXT, 'options' => ['notnull' => true, 'default' => 1, 'length' => 200]],
'allow_proposals' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => 'disallow', 'length' => 64]],
'proposals_expire' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
'vote_limit' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]],
diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php
index c01231b8d6..9825e41e7b 100644
--- a/lib/ResponseDefinitions.php
+++ b/lib/ResponseDefinitions.php
@@ -79,6 +79,7 @@
* access: string,
* allowComment: boolean,
* allowMaybe: boolean,
+ * chosenRank: String,
* allowProposals: string,
* anonymous: boolean,
* autoReminder: boolean,
diff --git a/lib/Service/OptionService.php b/lib/Service/OptionService.php
index 6fc12a7259..7b19990f20 100644
--- a/lib/Service/OptionService.php
+++ b/lib/Service/OptionService.php
@@ -108,7 +108,7 @@ public function addWithSequenceAndAutoVote(
public function add(int $pollId, SimpleOption $simpleOption, bool $voteYes = false): Option {
$this->getPoll($pollId, Poll::PERMISSION_OPTION_ADD);
- if ($this->poll->getType() === Poll::TYPE_TEXT) {
+ if ($this->poll->getType() === Poll::TYPE_TEXT or $this->poll->getType() === Poll::TYPE_GENERIC) {
$simpleOption->setOrder($this->getHighestOrder($pollId) + 1);
}
diff --git a/lib/Service/PollService.php b/lib/Service/PollService.php
index 84a1566740..022d0abb78 100644
--- a/lib/Service/PollService.php
+++ b/lib/Service/PollService.php
@@ -226,6 +226,7 @@ public function add(string $type, string $title, string $votingVariant = Poll::V
$this->poll->setExpire(0);
$this->poll->setAnonymousSafe(0);
$this->poll->setAllowMaybe(0);
+ $this->poll->setChosenRank('');
$this->poll->setVoteLimit(0);
$this->poll->setShowResults(Poll::SHOW_RESULTS_ALWAYS);
$this->poll->setDeleted(0);
@@ -418,6 +419,7 @@ public function clone(int $pollId): Poll {
// deanonymize cloned polls by default, to avoid locked anonymous polls
$this->poll->setAnonymous(0);
$this->poll->setAllowMaybe($origin->getAllowMaybe());
+ $this->poll->setChosenRank($origin->getChosenRank());
$this->poll->setVoteLimit($origin->getVoteLimit());
$this->poll->setShowResults($origin->getShowResults());
$this->poll->setAdminAccess($origin->getAdminAccess());
@@ -471,7 +473,7 @@ public function getValidEnum(): array {
* @psalm-return array{0: string, 1: string}
*/
private function getValidPollType(): array {
- return [Poll::TYPE_DATE, Poll::TYPE_TEXT];
+ return [Poll::TYPE_DATE, Poll::TYPE_TEXT, poll::TYPE_GENERIC];
}
/**
diff --git a/src/components/Cards/modules/CardAddProposals.vue b/src/components/Cards/modules/CardAddProposals.vue
index b49cd271c5..3d301b53f1 100644
--- a/src/components/Cards/modules/CardAddProposals.vue
+++ b/src/components/Cards/modules/CardAddProposals.vue
@@ -32,7 +32,7 @@ const optionAddDatesModalProps = {
diff --git a/src/components/Configuration/ConfigRankOptions.vue b/src/components/Configuration/ConfigRankOptions.vue
new file mode 100755
index 0000000000..0fc5f9bd09
--- /dev/null
+++ b/src/components/Configuration/ConfigRankOptions.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Navigation/PollNavigationItems.vue b/src/components/Navigation/PollNavigationItems.vue
index 0950805779..207bc0ade7 100644
--- a/src/components/Navigation/PollNavigationItems.vue
+++ b/src/components/Navigation/PollNavigationItems.vue
@@ -33,7 +33,7 @@ const sessionStore = useSessionStore()
"
:class="{ closed: poll.status.isExpired }">
-
+
diff --git a/src/components/Options/OptionItem.vue b/src/components/Options/OptionItem.vue
index 549beaecea..d052bab413 100644
--- a/src/components/Options/OptionItem.vue
+++ b/src/components/Options/OptionItem.vue
@@ -35,7 +35,7 @@ const pollStore = usePollStore()
diff --git a/src/components/PollList/PollItem.vue b/src/components/PollList/PollItem.vue
index fc2e947b6c..20814197b5 100644
--- a/src/components/PollList/PollItem.vue
+++ b/src/components/PollList/PollItem.vue
@@ -100,6 +100,10 @@ const descriptionLine = computed(() => {
v-if="poll.type === 'textPoll'"
class="item__type"
:title="pollTypes[poll.type].name" />
+
+
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SideBar/SideBarTabOptions.vue b/src/components/SideBar/SideBarTabOptions.vue
index 97a01cafad..e6e65992e5 100644
--- a/src/components/SideBar/SideBarTabOptions.vue
+++ b/src/components/SideBar/SideBarTabOptions.vue
@@ -94,7 +94,7 @@ onUnmounted(() => {
diff --git a/src/components/User/UserMenu.vue b/src/components/User/UserMenu.vue
index 9316c2a21e..9aad3db449 100644
--- a/src/components/User/UserMenu.vue
+++ b/src/components/User/UserMenu.vue
@@ -143,6 +143,10 @@ function changeView(): void {
preferencesStore.setViewTextPoll(
pollStore.viewMode === 'table-view' ? 'list-view' : 'table-view',
)
+ } else if (pollStore.type === 'genericPoll' ) {
+ preferencesStore.setViewTextPoll(
+ pollStore.viewMode === 'table-view' ? 'list-view' : 'table-view',
+ )
}
}
diff --git a/src/components/VoteTable/VoteIndicator.vue b/src/components/VoteTable/VoteIndicator.vue
index 31485c1fc3..6bf296b698 100644
--- a/src/components/VoteTable/VoteIndicator.vue
+++ b/src/components/VoteTable/VoteIndicator.vue
@@ -4,11 +4,15 @@
-->
-
-
+
+
+
+ {{ selectedRank }}
+
+
+
+
+
+
diff --git a/src/components/VoteTable/VoteItem.vue b/src/components/VoteTable/VoteItem.vue
index a5cafa97ef..b2e669fb1b 100644
--- a/src/components/VoteTable/VoteItem.vue
+++ b/src/components/VoteTable/VoteItem.vue
@@ -66,9 +66,26 @@ const nextAnswer = computed(() => {
const isValidUser = computed(() => user.id !== '' && user.id !== null)
-/**
- *
- */
+async function handleRankSelected(rank){
+ if (isVotable.value) {
+ try {
+ await votesStore.set({
+ option,
+ setTo: String(rank),
+ });
+ showSuccess(t('polls', 'Vote saved'), { timeout: 2000 });
+ } catch (error) {
+ if ((error as AxiosError).status === 409) {
+ showError(t('polls', 'Vote already booked out'));
+ } else {
+ showError(t('polls', 'Error saving vote'));
+ }
+ }
+ } else {
+ showError(t('polls', 'Error saving vote'));
+ }
+}
+
async function setVote() {
if (isVotable.value) {
try {
diff --git a/src/components/VoteTable/VoteTable.vue b/src/components/VoteTable/VoteTable.vue
index 3e3ad02f67..f2a536a8ad 100644
--- a/src/components/VoteTable/VoteTable.vue
+++ b/src/components/VoteTable/VoteTable.vue
@@ -143,8 +143,10 @@ const showCalendarPeek = computed(
overflow: scroll;
.vote-cell {
- padding: 0.4rem;
- display: flex;
+ display: grid;
+ place-items: center; /* Centre tout le contenu */
+ grid-template-columns: 1fr;
+ width: 100%;
}
.participant {
@@ -282,6 +284,39 @@ const showCalendarPeek = computed(
grid-column: 2;
flex-direction: column;
}
+ .vote-cell-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ padding: 4px;
+}
+
+.vote-select {
+ width: 100%;
+ max-width: 120px;
+ min-width: 80px;
+ margin: 0 auto;
+ padding: 8px 12px;
+ text-align: center;
+ text-align-last: center; /* Centrage pour Firefox */
+ border-radius: 8px;
+ border: 1px solid var(--color-border);
+ background: var(--color-main-background);
+ cursor: pointer;
+ appearance: none;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: var(--color-primary);
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px var(--color-primary-light);
+ }
+}
.vote-cell {
grid-column: 3;
diff --git a/src/stores/poll.ts b/src/stores/poll.ts
index 1d7efa49cc..2990cdea9a 100644
--- a/src/stores/poll.ts
+++ b/src/stores/poll.ts
@@ -35,7 +35,7 @@ import { useSharesStore } from './shares.ts'
import { useCommentsStore } from './comments.ts'
import { AxiosError } from '@nextcloud/axios'
-export type PollType = 'textPoll' | 'datePoll'
+export type PollType = 'textPoll' | 'datePoll' | 'genericPoll'
type PollTypesType = {
name: string
@@ -45,9 +45,12 @@ export const pollTypes: Record
= {
textPoll: {
name: t('polls', 'Text poll'),
},
+ genericPoll: {
+ name: t('polls', 'Generic poll'),
+ },
datePoll: {
name: t('polls', 'Date poll'),
- },
+ }
}
export type VoteVariant = 'simple'
@@ -65,6 +68,7 @@ export type PollConfiguration = {
access: AccessType
allowComment: boolean
allowMaybe: boolean
+ chosenRank: string
allowProposals: AllowProposals
anonymous: boolean
autoReminder: boolean
@@ -154,6 +158,8 @@ const markedPrefix = {
prefix: 'desc-',
}
+const DEFAULT_CHOSEN_RANK = [] ;
+
export const usePollStore = defineStore('poll', {
state: (): Poll => ({
id: 0,
@@ -166,6 +172,7 @@ export const usePollStore = defineStore('poll', {
access: 'private',
allowComment: false,
allowMaybe: false,
+ chosenRank: JSON.stringify(DEFAULT_CHOSEN_RANK),
allowProposals: 'disallow',
anonymous: false,
autoReminder: false,
@@ -243,12 +250,27 @@ export const usePollStore = defineStore('poll', {
}),
getters: {
+
+ getChosenRank(): string[] {
+ try {
+ const parsed = JSON.parse(this.configuration.chosenRank || '[]')
+ return Array.isArray(parsed) ? parsed : []
+ } catch {
+ return DEFAULT_CHOSEN_RANK;
+ }
+ },
+
+
viewMode(state): ViewMode {
const preferencesStore = usePreferencesStore()
if (state.type === 'textPoll') {
return preferencesStore.viewTextPoll
}
+ if (state.type === 'genericPoll') {
+ return preferencesStore.viewTextPoll
+ }
+
if (state.type === 'datePoll') {
return preferencesStore.viewDatePoll
}
@@ -352,6 +374,15 @@ export const usePollStore = defineStore('poll', {
},
actions: {
+
+ setChosenRank(ranks: string[]) {
+ const validItems = Array.isArray(ranks)
+ ? ranks.map(item => String(item).trim()) // Correction ici
+ .filter(item => item !== '') // Filtre les chaînes vides
+ : [];
+ this.configuration.chosenRank = JSON.stringify(validItems.sort());
+ },
+
reset(): void {
this.$reset()
},
diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue
index 7380a66727..5b86e37de8 100644
--- a/src/views/Dashboard.vue
+++ b/src/views/Dashboard.vue
@@ -58,6 +58,7 @@ onMounted(() => {
+
diff --git a/src/views/Vote.vue b/src/views/Vote.vue
index a17706059a..94baf54c70 100644
--- a/src/views/Vote.vue
+++ b/src/views/Vote.vue
@@ -206,6 +206,7 @@ onUnmounted(() => {
v-bind="emptyContentProps">
+