feat: clean up server power action buttons (#5836)
This commit is contained in:
@@ -1,13 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="joined-buttons">
|
<div class="joined-buttons">
|
||||||
<ButtonStyled :color="color">
|
<ButtonStyled
|
||||||
<button :disabled="disabled" @click="handlePrimaryAction">
|
:color="color"
|
||||||
|
:size="size"
|
||||||
|
:class="{ 'joined-buttons__primary--muted': primaryMuted }"
|
||||||
|
>
|
||||||
|
<button :disabled="primaryDisabledResolved" @click="handlePrimaryAction">
|
||||||
<component :is="primaryAction.icon" v-if="primaryAction.icon" aria-hidden="true" />
|
<component :is="primaryAction.icon" v-if="primaryAction.icon" aria-hidden="true" />
|
||||||
{{ primaryAction.label }}
|
{{ primaryAction.label }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled v-if="dropdownActions.length > 0" :color="color">
|
<ButtonStyled
|
||||||
<OverflowMenu class="btn-dropdown-animation" :options="dropdownOptions" :disabled="disabled">
|
v-if="dropdownActions.length > 0"
|
||||||
|
:color="color"
|
||||||
|
:size="size"
|
||||||
|
class="joined-buttons__dropdown"
|
||||||
|
>
|
||||||
|
<OverflowMenu
|
||||||
|
class="btn-dropdown-animation !w-10"
|
||||||
|
:options="dropdownOptions"
|
||||||
|
:disabled="dropdownDisabledResolved"
|
||||||
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
<template v-for="action in dropdownActions" :key="action.id" #[action.id]>
|
<template v-for="action in dropdownActions" :key="action.id" #[action.id]>
|
||||||
<component :is="action.icon" v-if="action.icon" aria-hidden="true" />
|
<component :is="action.icon" v-if="action.icon" aria-hidden="true" />
|
||||||
@@ -40,14 +53,25 @@ export interface JoinedButtonAction {
|
|||||||
interface Props {
|
interface Props {
|
||||||
actions: JoinedButtonAction[]
|
actions: JoinedButtonAction[]
|
||||||
color?: Colors
|
color?: Colors
|
||||||
|
size?: 'standard' | 'large' | 'small'
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
primaryDisabled?: boolean
|
||||||
|
dropdownDisabled?: boolean
|
||||||
|
primaryMuted?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
color: 'standard',
|
color: 'standard',
|
||||||
|
size: 'standard',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
primaryDisabled: undefined,
|
||||||
|
dropdownDisabled: undefined,
|
||||||
|
primaryMuted: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const primaryDisabledResolved = computed(() => props.primaryDisabled ?? props.disabled)
|
||||||
|
const dropdownDisabledResolved = computed(() => props.dropdownDisabled ?? props.disabled)
|
||||||
|
|
||||||
const primaryAction = computed(() => props.actions[0])
|
const primaryAction = computed(() => props.actions[0])
|
||||||
|
|
||||||
const dropdownActions = computed(() => props.actions.slice(1))
|
const dropdownActions = computed(() => props.actions.slice(1))
|
||||||
@@ -84,7 +108,7 @@ const dropdownOptions = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
function handlePrimaryAction() {
|
function handlePrimaryAction() {
|
||||||
if (primaryAction.value && !props.disabled) {
|
if (primaryAction.value && !primaryDisabledResolved.value) {
|
||||||
primaryAction.value.action()
|
primaryAction.value.action()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,4 +142,8 @@ function handlePrimaryAction() {
|
|||||||
.btn-dropdown-animation {
|
.btn-dropdown-animation {
|
||||||
padding: 0.5rem !important;
|
padding: 0.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.joined-buttons__primary--muted {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,50 +7,46 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else-if="showRestartButton">
|
||||||
<ButtonStyled v-if="showStopButton" type="standard" color="red" size="large">
|
<ButtonStyled type="standard" color="orange" size="large">
|
||||||
<button
|
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||||
v-tooltip="busyTooltip"
|
<UpdatedIcon />
|
||||||
:disabled="!canTakeAction"
|
<span>{{ primaryActionText }}</span>
|
||||||
@click="initiateAction('Stop')"
|
|
||||||
>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<StopCircleIcon class="h-5 w-5" />
|
|
||||||
<span>Stop</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<div v-if="showRestartDropdown" class="joined-buttons">
|
<JoinedButtons
|
||||||
<ButtonStyled type="standard" color="orange" size="large">
|
color="red"
|
||||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
size="large"
|
||||||
<UpdatedIcon />
|
:actions="stopSplitActions"
|
||||||
<span>{{ primaryActionText }}</span>
|
:primary-disabled="!canTakeAction"
|
||||||
</button>
|
:dropdown-disabled="!canKill"
|
||||||
</ButtonStyled>
|
>
|
||||||
<ButtonStyled type="standard" color="orange" size="large">
|
<template #kill_server>
|
||||||
<OverflowMenu
|
<SlashIcon class="h-5 w-5" />
|
||||||
v-tooltip="busyTooltip"
|
Kill server
|
||||||
:disabled="!canTakeAction"
|
</template>
|
||||||
:options="[
|
</JoinedButtons>
|
||||||
{
|
</template>
|
||||||
id: 'kill_server',
|
|
||||||
action: () => initiateAction('Kill'),
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="w-0 text-xl relative top-0.5 right-2.5">
|
|
||||||
<DropdownIcon />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #kill_server>
|
<template v-else-if="isStopping">
|
||||||
<SlashIcon class="h-5 w-5" />
|
<JoinedButtons
|
||||||
Kill server
|
color="red"
|
||||||
</template>
|
size="large"
|
||||||
</OverflowMenu>
|
:actions="stopSplitActions"
|
||||||
</ButtonStyled>
|
:primary-disabled="true"
|
||||||
</div>
|
:dropdown-disabled="!canKill"
|
||||||
<ButtonStyled v-else type="standard" color="brand" size="large">
|
:primary-muted="true"
|
||||||
|
>
|
||||||
|
<template #kill_server>
|
||||||
|
<SlashIcon class="h-5 w-5" />
|
||||||
|
Kill server
|
||||||
|
</template>
|
||||||
|
</JoinedButtons>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<ButtonStyled type="standard" color="brand" size="large">
|
||||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||||
<PlayIcon />
|
<PlayIcon />
|
||||||
<span>{{ primaryActionText }}</span>
|
<span>{{ primaryActionText }}</span>
|
||||||
@@ -63,7 +59,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
DropdownIcon,
|
|
||||||
LoaderCircleIcon,
|
LoaderCircleIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
SlashIcon,
|
SlashIcon,
|
||||||
@@ -72,7 +67,7 @@ import {
|
|||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { ButtonStyled, OverflowMenu } from '#ui/components'
|
import { ButtonStyled, type JoinedButtonAction, JoinedButtons } from '#ui/components'
|
||||||
|
|
||||||
import { useServerPowerAction } from './use-server-power-action'
|
import { useServerPowerAction } from './use-server-power-action'
|
||||||
|
|
||||||
@@ -87,9 +82,11 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
isInstalling,
|
isInstalling,
|
||||||
showStopButton,
|
isStopping,
|
||||||
|
showRestartButton,
|
||||||
busyTooltip,
|
busyTooltip,
|
||||||
canTakeAction,
|
canTakeAction,
|
||||||
|
canKill,
|
||||||
primaryActionText,
|
primaryActionText,
|
||||||
initiateAction,
|
initiateAction,
|
||||||
handlePrimaryAction,
|
handlePrimaryAction,
|
||||||
@@ -97,31 +94,18 @@ const {
|
|||||||
disabled: computed(() => props.disabled),
|
disabled: computed(() => props.disabled),
|
||||||
})
|
})
|
||||||
|
|
||||||
const showRestartDropdown = computed(() => primaryActionText.value === 'Restart')
|
const stopSplitActions = computed<JoinedButtonAction[]>(() => [
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: isStopping.value ? 'Stopping' : 'Stop',
|
||||||
|
icon: StopCircleIcon,
|
||||||
|
action: () => initiateAction('Stop'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kill_server',
|
||||||
|
label: 'Kill server',
|
||||||
|
icon: SlashIcon,
|
||||||
|
action: () => initiateAction('Kill'),
|
||||||
|
},
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.joined-buttons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joined-buttons > :deep(.btn) {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joined-buttons > :deep(.btn:first-child) {
|
|
||||||
border-top-left-radius: var(--radius-md);
|
|
||||||
border-bottom-left-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.joined-buttons > :deep(.btn:last-child) {
|
|
||||||
border-top-right-radius: var(--radius-md);
|
|
||||||
border-bottom-right-radius: var(--radius-md);
|
|
||||||
margin-left: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joined-buttons > :deep(.btn:not(:last-child)) {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -20,15 +20,24 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
|
|||||||
const isStopping = computed(() => powerState.value === 'stopping')
|
const isStopping = computed(() => powerState.value === 'stopping')
|
||||||
const isStarting = computed(() => powerState.value === 'starting')
|
const isStarting = computed(() => powerState.value === 'starting')
|
||||||
const isTransitioning = computed(() => isStarting.value || isStopping.value)
|
const isTransitioning = computed(() => isStarting.value || isStopping.value)
|
||||||
const showStopButton = computed(() => isRunning.value || isStarting.value)
|
|
||||||
|
const showStopSplit = computed(() => isRunning.value || isStarting.value || isStopping.value)
|
||||||
|
const showRestartButton = computed(() => isRunning.value || isStarting.value)
|
||||||
|
|
||||||
|
const isBlockedByPropsOrBusy = computed(
|
||||||
|
() => Boolean(options?.disabled?.value) || busyReasons.value.length > 0,
|
||||||
|
)
|
||||||
|
|
||||||
const busyTooltip = computed(() => {
|
const busyTooltip = computed(() => {
|
||||||
if (isStarting.value) return 'Your server is starting'
|
if (isStarting.value) return 'Your server is starting'
|
||||||
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined
|
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const canTakeAction = computed(
|
const canTakeAction = computed(() => !isTransitioning.value && !isBlockedByPropsOrBusy.value)
|
||||||
() => !isTransitioning.value && !options?.disabled?.value && busyReasons.value.length === 0,
|
|
||||||
|
const canKill = computed(
|
||||||
|
() =>
|
||||||
|
!isBlockedByPropsOrBusy.value && (isStopping.value || isRunning.value || isStarting.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const primaryActionText = computed(() => {
|
const primaryActionText = computed(() => {
|
||||||
@@ -57,7 +66,11 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initiateAction(action: PowerAction) {
|
function initiateAction(action: PowerAction) {
|
||||||
if (!canTakeAction.value) return
|
if (action === 'Kill') {
|
||||||
|
if (!canKill.value) return
|
||||||
|
} else {
|
||||||
|
if (!canTakeAction.value) return
|
||||||
|
}
|
||||||
void sendPowerAction(action)
|
void sendPowerAction(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +83,11 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
|
|||||||
isRunning,
|
isRunning,
|
||||||
isStopping,
|
isStopping,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
showStopButton,
|
showStopSplit,
|
||||||
|
showRestartButton,
|
||||||
busyTooltip,
|
busyTooltip,
|
||||||
canTakeAction,
|
canTakeAction,
|
||||||
|
canKill,
|
||||||
primaryActionText,
|
primaryActionText,
|
||||||
sendPowerAction,
|
sendPowerAction,
|
||||||
initiateAction,
|
initiateAction,
|
||||||
|
|||||||
122
packages/ui/src/stories/base/JoinedButtons.stories.ts
Normal file
122
packages/ui/src/stories/base/JoinedButtons.stories.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { PlayIcon, SlashIcon, StopCircleIcon, UpdatedIcon } from '@modrinth/assets'
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import JoinedButtons from '../../components/base/JoinedButtons.vue'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Base/JoinedButtons',
|
||||||
|
component: JoinedButtons,
|
||||||
|
argTypes: {
|
||||||
|
color: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['standard', 'brand', 'red', 'orange', 'green', 'blue', 'purple'],
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
control: 'select',
|
||||||
|
options: ['small', 'standard', 'large'],
|
||||||
|
},
|
||||||
|
disabled: { control: 'boolean' },
|
||||||
|
primaryDisabled: { control: 'boolean' },
|
||||||
|
dropdownDisabled: { control: 'boolean' },
|
||||||
|
primaryMuted: { control: 'boolean' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof JoinedButtons>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Start: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'brand',
|
||||||
|
size: 'large',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'start',
|
||||||
|
label: 'Start',
|
||||||
|
icon: PlayIcon,
|
||||||
|
action: () => console.log('Start'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StopWithKill: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'red',
|
||||||
|
size: 'large',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: 'Stop',
|
||||||
|
icon: StopCircleIcon,
|
||||||
|
action: () => console.log('Stop'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kill_server',
|
||||||
|
label: 'Kill server',
|
||||||
|
icon: SlashIcon,
|
||||||
|
action: () => console.log('Kill'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Stopping: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'red',
|
||||||
|
size: 'large',
|
||||||
|
primaryDisabled: true,
|
||||||
|
primaryMuted: true,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: 'Stopping',
|
||||||
|
icon: StopCircleIcon,
|
||||||
|
action: () => console.log('Stop'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kill_server',
|
||||||
|
label: 'Kill server',
|
||||||
|
icon: SlashIcon,
|
||||||
|
action: () => console.log('Kill'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Restart: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'orange',
|
||||||
|
size: 'large',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'restart',
|
||||||
|
label: 'Restart',
|
||||||
|
icon: UpdatedIcon,
|
||||||
|
action: () => console.log('Restart'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Disabled: Story = {
|
||||||
|
args: {
|
||||||
|
color: 'red',
|
||||||
|
size: 'large',
|
||||||
|
disabled: true,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: 'Stop',
|
||||||
|
icon: StopCircleIcon,
|
||||||
|
action: () => console.log('Stop'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kill_server',
|
||||||
|
label: 'Kill server',
|
||||||
|
icon: SlashIcon,
|
||||||
|
action: () => console.log('Kill'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user