mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 20:48:56 +01:00
feat: Vote panel UI cleanup; auto-close fix (#96166)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user