feat: Vote panel UI cleanup; auto-close fix (#96166)

This commit is contained in:
mcbalaam
2026-05-27 20:42:04 -04:00
committed by GitHub
parent 509860f2a7
commit 052b3a654d
2 changed files with 211 additions and 248 deletions
+2 -3
View File
@@ -284,6 +284,7 @@ SUBSYSTEM_DEF(vote)
log_admin("[key_name(toggle_initiator)] [text_verb] Dead Vote.")
message_admins("[key_name_admin(toggle_initiator)] [text_verb] Dead Vote.")
SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Dead Vote", text_verb))
update_static_data_for_all_viewers()
/datum/controller/subsystem/vote/ui_state()
return GLOB.always_state
@@ -312,8 +313,6 @@ SUBSYSTEM_DEF(vote)
"multiSelection" = current_vote?.choices_by_ckey,
)
data["voting"]= is_lower_admin ? voting : list()
var/list/all_vote_data = list()
for(var/vote_name in possible_votes)
var/datum/vote/vote = possible_votes[vote_name]
@@ -356,6 +355,7 @@ SUBSYSTEM_DEF(vote)
/datum/controller/subsystem/vote/ui_static_data(mob/user)
var/list/data = list()
data["VoteCD"] = CONFIG_GET(number/vote_delay)
data["deadVoteEnabled"] = CONFIG_GET(flag/no_dead_vote)
return data
/datum/controller/subsystem/vote/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
@@ -374,7 +374,6 @@ SUBSYSTEM_DEF(vote)
voter.log_message("cancelled a vote.", LOG_ADMIN)
message_admins("[key_name_admin(voter)] has cancelled the current vote.")
SStgui.close_uis(src)
reset()
return TRUE
+209 -245
View File
@@ -1,13 +1,14 @@
import {
BlockQuote,
Box,
Button,
Collapsible,
Dimmer,
Icon,
LabeledList,
NoticeBox,
Section,
Stack,
Tooltip,
} from 'tgui-core/components';
import type { BooleanLike } from 'tgui-core/react';
@@ -60,18 +61,15 @@ type Data = {
currentVote: ActiveVote;
possibleVotes: Vote[];
user: UserData;
voting: string[];
LastVoteTime: number;
VoteCD: number;
deadVoteEnabled: BooleanLike;
};
export const VotePanel = (props) => {
const { act, data } = useBackend<Data>();
const { currentVote, user, LastVoteTime, VoteCD } = data;
/**
* Adds the voting type to title if there is an ongoing vote.
*/
let windowTitle = 'Vote';
if (currentVote) {
windowTitle +=
@@ -84,37 +82,49 @@ export const VotePanel = (props) => {
return (
<Window title={windowTitle} width={400} height={500}>
<Window.Content>
<Stack fill vertical>
<Section
title="Create Vote"
buttons={
!!user.isLowerAdmin && (
<Stack>
<Stack.Item>
<Button
icon="refresh"
content="Reset Cooldown"
disabled={LastVoteTime + VoteCD <= 0}
onClick={() => act('resetCooldown')}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="skull"
content="Toggle dead vote"
disabled={!user.isUpperAdmin}
onClick={() => act('toggleDeadVote')}
/>
</Stack.Item>
</Stack>
)
}
>
<VoteOptions />
{!!user.isLowerAdmin && currentVote && <VotersList />}
</Section>
<ChoicesPanel />
<TimePanel />
<Stack vertical fill>
<Stack.Item>
<Section
title="New Vote"
buttons={
!!user.isLowerAdmin && (
<Stack>
<Stack.Item>
<Button
icon="refresh"
disabled={LastVoteTime + VoteCD <= 0}
onClick={() => act('resetCooldown')}
>
Reset cooldown
</Button>
</Stack.Item>
<Stack.Item>
<Button.Checkbox
disabled={!user.isUpperAdmin}
onClick={() => act('toggleDeadVote')}
checked={!data.deadVoteEnabled}
color="primary"
>
Dead votes
</Button.Checkbox>
</Stack.Item>
</Stack>
)
}
>
<VoteOptions />
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill scrollable title="Active Vote">
<ChoicesPanel />
</Section>
</Stack.Item>
<Stack.Item>
<Section>
<TimePanel />
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
@@ -137,239 +147,193 @@ const VoteOptionDimmer = (props) => {
);
};
/**
* The create vote options menu. Only upper admins can disable voting.
* @returns A section visible to everyone with vote options.
*/
const VoteOptions = (props) => {
const { act, data } = useBackend<Data>();
const { possibleVotes, user, LastVoteTime, VoteCD } = data;
return (
<Stack.Item>
<Collapsible title="Start a Vote">
<Section>
{LastVoteTime + VoteCD > 0 && <VoteOptionDimmer />}
<Stack vertical justify="space-between">
{possibleVotes.map((option) => (
<Stack.Item key={option.name}>
<Stack>
{!!user.isLowerAdmin && (
<Stack.Item>
<Button.Checkbox
width={7}
color="red"
checked={option.config === VoteConfig.Enabled}
disabled={
!user.isUpperAdmin ||
option.config === VoteConfig.None
}
tooltip={
option.config === VoteConfig.None
? 'This vote cannot be disabled.'
: null
}
content={
option.config === VoteConfig.Enabled
? 'Enabled'
: 'Disabled'
}
onClick={() =>
act('toggleVote', {
voteName: option.name,
})
}
/>
</Stack.Item>
)}
<Stack.Item>
<Button
width={12}
textAlign={'center'}
disabled={!option.canBeInitiated}
tooltip={option.message}
content={option.name}
onClick={() =>
act('callVote', {
voteName: option.name,
})
}
/>
</Stack.Item>
</Stack>
{LastVoteTime + VoteCD > 0 && <VoteOptionDimmer />}
<Stack vertical justify="space-between">
{possibleVotes.map((option) => (
<Stack.Item key={option.name}>
<Stack>
{!!user.isLowerAdmin && (
<Stack.Item>
<Button.Checkbox
color="primary"
checked={
option.config === VoteConfig.Enabled ||
option.config === VoteConfig.None
}
disabled={
!user.isUpperAdmin || option.config === VoteConfig.None
}
tooltip={
option.config === VoteConfig.None
? 'This vote cannot be disabled.'
: null
}
onClick={() =>
act('toggleVote', {
voteName: option.name,
})
}
>
Active
</Button.Checkbox>
</Stack.Item>
)}
<Stack.Item>
<Button
disabled={!option.canBeInitiated}
onClick={() =>
act('callVote', {
voteName: option.name,
})
}
icon="play"
/>
</Stack.Item>
))}
</Stack>
</Section>
</Collapsible>
<Stack.Item>
<Tooltip content={option.message}>
<BlockQuote style={{ lineHeight: '1.7em' }}>
{option.name} Vote
</BlockQuote>
</Tooltip>
</Stack.Item>
</Stack>
</Stack.Item>
))}
</Stack>
</Stack.Item>
);
};
/**
* View Voters by ckey. Admin only.
* @returns A collapsible list of voters
*/
const VotersList = (props) => {
const { data } = useBackend<Data>();
return (
<Stack.Item>
<Collapsible
title={`View Active Voters${
data.voting.length ? ` (${data.voting.length})` : ''
}`}
>
<Section height={4} fill scrollable>
{data.voting.map((voter) => {
return <Box key={voter}>{voter}</Box>;
})}
</Section>
</Collapsible>
</Stack.Item>
);
};
/**
* The choices panel which displays all options in the list.
* @returns A section visible to all users.
*/
const ChoicesPanel = (props) => {
const { act, data } = useBackend<Data>();
const { currentVote, user } = data;
return (
<Stack.Item grow>
<Section fill scrollable title="Active Vote">
{currentVote && currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
<NoticeBox success>Select one option</NoticeBox>
) : null}
{currentVote &&
currentVote.choices.length !== 0 &&
currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
<LabeledList>
{currentVote.choices.map((choice) => (
<Box key={choice.name}>
<LabeledList.Item
label={choice.name.replace(/^\w/, (c) => c.toUpperCase())}
textAlign="right"
buttons={
<Button
tooltip={
user.isGhost && 'Ghost voting was disabled by an admin.'
}
disabled={
user.singleSelection === choice.name || user.isGhost
}
onClick={() => {
act('voteSingle', { voteOption: choice.name });
}}
>
Vote
</Button>
}
>
{user.singleSelection &&
choice.name === user.singleSelection && (
<Icon
align="right"
mr={2}
color="green"
name="vote-yea"
/>
)}
{currentVote.displayStatistics
? `${choice.votes} Votes`
: null}
</LabeledList.Item>
<LabeledList.Divider />
</Box>
))}
</LabeledList>
) : null}
{currentVote && currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
<NoticeBox success>Select any number of options</NoticeBox>
) : null}
{currentVote &&
currentVote.choices.length !== 0 &&
currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
<LabeledList>
{currentVote.choices.map((choice) => (
<Box key={choice.name}>
<LabeledList.Item
label={choice.name.replace(/^\w/, (c) => c.toUpperCase())}
textAlign="right"
buttons={
<Button
tooltip={
user.isGhost && 'Ghost voting was disabled by an admin.'
}
disabled={user.isGhost}
onClick={() => {
act('voteMulti', { voteOption: choice.name });
}}
>
Vote
</Button>
}
>
{user.multiSelection &&
user.multiSelection[user.ckey.concat(choice.name)] === 1 ? (
<Icon align="right" mr={2} color="blue" name="vote-yea" />
) : null}
{choice.votes} Votes
</LabeledList.Item>
<LabeledList.Divider />
</Box>
))}
</LabeledList>
) : null}
{currentVote ? null : <NoticeBox>No vote active!</NoticeBox>}
</Section>
</Stack.Item>
<>
{currentVote && currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
<NoticeBox success>Select one option</NoticeBox>
) : null}
{currentVote &&
currentVote.choices.length !== 0 &&
currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
<LabeledList>
{currentVote.choices.map((choice) => (
<Box key={choice.name}>
<LabeledList.Item
label={choice.name.replace(/^\w/, (c) => c.toUpperCase())}
textAlign="right"
buttons={
<Button
tooltip={
user.isGhost && 'Ghost voting was disabled by an admin.'
}
disabled={
user.singleSelection === choice.name || user.isGhost
}
onClick={() => {
act('voteSingle', { voteOption: choice.name });
}}
>
Vote
</Button>
}
>
{user.singleSelection &&
choice.name === user.singleSelection && (
<Icon align="right" mr={2} color="green" name="vote-yea" />
)}
{currentVote.displayStatistics ? `${choice.votes} Votes` : null}
</LabeledList.Item>
<LabeledList.Divider />
</Box>
))}
</LabeledList>
) : null}
{currentVote && currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
<NoticeBox success>Select any number of options</NoticeBox>
) : null}
{currentVote &&
currentVote.choices.length !== 0 &&
currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
<LabeledList>
{currentVote.choices.map((choice) => (
<Box key={choice.name}>
<LabeledList.Item
label={choice.name.replace(/^\w/, (c) => c.toUpperCase())}
textAlign="right"
buttons={
<Button
tooltip={
user.isGhost && 'Ghost voting was disabled by an admin.'
}
disabled={user.isGhost}
onClick={() => {
act('voteMulti', { voteOption: choice.name });
}}
>
Vote
</Button>
}
>
{user.multiSelection &&
user.multiSelection[user.ckey.concat(choice.name)] === 1 ? (
<Icon align="right" mr={2} color="blue" name="vote-yea" />
) : null}
{choice.votes} Votes
</LabeledList.Item>
<LabeledList.Divider />
</Box>
))}
</LabeledList>
) : null}
{currentVote ? null : <NoticeBox>No vote active!</NoticeBox>}
</>
);
};
/**
* Countdown timer at the bottom. Includes a cancel vote option for admins.
* @returns A section visible to everyone.
*/
const TimePanel = (props) => {
const { act, data } = useBackend<Data>();
const { currentVote, user } = data;
return (
<Stack.Item mt={1}>
<Section>
<Stack justify="space-between">
<Box fontSize={1.5}>
Time Remaining:&nbsp;
{currentVote?.timeRemaining || 0}s
</Box>
{!!user.isLowerAdmin && (
<Stack>
<Stack.Item>
<Button
color="green"
disabled={!user.isLowerAdmin || !currentVote}
onClick={() => act('endNow')}
>
End Now
</Button>
</Stack.Item>
<Stack.Item>
<Button
color="red"
disabled={!user.isLowerAdmin || !currentVote}
onClick={() => act('cancel')}
>
Cancel Vote
</Button>
</Stack.Item>
</Stack>
)}
</Stack>
</Section>
<Stack.Item>
<Stack justify="space-between">
<Box fontSize={1.5}>
{currentVote
? `Time remaining: ${currentVote.timeRemaining}s`
: 'No current vote'}
</Box>
{!!user.isLowerAdmin && (
<Stack>
<Stack.Item>
<Button
color="green"
disabled={!user.isLowerAdmin || !currentVote}
onClick={() => act('endNow')}
style={{ lineHeight: '1.8em' }}
>
End Now
</Button>
</Stack.Item>
<Stack.Item>
<Button
color="red"
disabled={!user.isLowerAdmin || !currentVote}
onClick={() => act('cancel')}
style={{ lineHeight: '1.8em' }}
>
Cancel
</Button>
</Stack.Item>
</Stack>
)}
</Stack>
</Stack.Item>
);
};