feat: clean up server power action buttons (#5836)
This commit is contained in:
@@ -1,13 +1,26 @@
|
||||
<template>
|
||||
<div class="joined-buttons">
|
||||
<ButtonStyled :color="color">
|
||||
<button :disabled="disabled" @click="handlePrimaryAction">
|
||||
<ButtonStyled
|
||||
: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" />
|
||||
{{ primaryAction.label }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="dropdownActions.length > 0" :color="color">
|
||||
<OverflowMenu class="btn-dropdown-animation" :options="dropdownOptions" :disabled="disabled">
|
||||
<ButtonStyled
|
||||
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 />
|
||||
<template v-for="action in dropdownActions" :key="action.id" #[action.id]>
|
||||
<component :is="action.icon" v-if="action.icon" aria-hidden="true" />
|
||||
@@ -40,14 +53,25 @@ export interface JoinedButtonAction {
|
||||
interface Props {
|
||||
actions: JoinedButtonAction[]
|
||||
color?: Colors
|
||||
size?: 'standard' | 'large' | 'small'
|
||||
disabled?: boolean
|
||||
primaryDisabled?: boolean
|
||||
dropdownDisabled?: boolean
|
||||
primaryMuted?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: 'standard',
|
||||
size: 'standard',
|
||||
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 dropdownActions = computed(() => props.actions.slice(1))
|
||||
@@ -84,7 +108,7 @@ const dropdownOptions = computed(() =>
|
||||
)
|
||||
|
||||
function handlePrimaryAction() {
|
||||
if (primaryAction.value && !props.disabled) {
|
||||
if (primaryAction.value && !primaryDisabledResolved.value) {
|
||||
primaryAction.value.action()
|
||||
}
|
||||
}
|
||||
@@ -118,4 +142,8 @@ function handlePrimaryAction() {
|
||||
.btn-dropdown-animation {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
.joined-buttons__primary--muted {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,50 +7,46 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<template v-else>
|
||||
<ButtonStyled v-if="showStopButton" type="standard" color="red" size="large">
|
||||
<button
|
||||
v-tooltip="busyTooltip"
|
||||
:disabled="!canTakeAction"
|
||||
@click="initiateAction('Stop')"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<StopCircleIcon class="h-5 w-5" />
|
||||
<span>Stop</span>
|
||||
</div>
|
||||
<template v-else-if="showRestartButton">
|
||||
<ButtonStyled type="standard" color="orange" size="large">
|
||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||
<UpdatedIcon />
|
||||
<span>{{ primaryActionText }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<div v-if="showRestartDropdown" class="joined-buttons">
|
||||
<ButtonStyled type="standard" color="orange" size="large">
|
||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||
<UpdatedIcon />
|
||||
<span>{{ primaryActionText }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="standard" color="orange" size="large">
|
||||
<OverflowMenu
|
||||
v-tooltip="busyTooltip"
|
||||
:disabled="!canTakeAction"
|
||||
:options="[
|
||||
{
|
||||
id: 'kill_server',
|
||||
action: () => initiateAction('Kill'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="w-0 text-xl relative top-0.5 right-2.5">
|
||||
<DropdownIcon />
|
||||
</div>
|
||||
<JoinedButtons
|
||||
color="red"
|
||||
size="large"
|
||||
:actions="stopSplitActions"
|
||||
:primary-disabled="!canTakeAction"
|
||||
:dropdown-disabled="!canKill"
|
||||
>
|
||||
<template #kill_server>
|
||||
<SlashIcon class="h-5 w-5" />
|
||||
Kill server
|
||||
</template>
|
||||
</JoinedButtons>
|
||||
</template>
|
||||
|
||||
<template #kill_server>
|
||||
<SlashIcon class="h-5 w-5" />
|
||||
Kill server
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ButtonStyled v-else type="standard" color="brand" size="large">
|
||||
<template v-else-if="isStopping">
|
||||
<JoinedButtons
|
||||
color="red"
|
||||
size="large"
|
||||
:actions="stopSplitActions"
|
||||
:primary-disabled="true"
|
||||
:dropdown-disabled="!canKill"
|
||||
: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">
|
||||
<PlayIcon />
|
||||
<span>{{ primaryActionText }}</span>
|
||||
@@ -63,7 +59,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownIcon,
|
||||
LoaderCircleIcon,
|
||||
PlayIcon,
|
||||
SlashIcon,
|
||||
@@ -72,7 +67,7 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
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'
|
||||
|
||||
@@ -87,9 +82,11 @@ const props = withDefaults(
|
||||
|
||||
const {
|
||||
isInstalling,
|
||||
showStopButton,
|
||||
isStopping,
|
||||
showRestartButton,
|
||||
busyTooltip,
|
||||
canTakeAction,
|
||||
canKill,
|
||||
primaryActionText,
|
||||
initiateAction,
|
||||
handlePrimaryAction,
|
||||
@@ -97,31 +94,18 @@ const {
|
||||
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>
|
||||
|
||||
<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 isStarting = computed(() => powerState.value === 'starting')
|
||||
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(() => {
|
||||
if (isStarting.value) return 'Your server is starting'
|
||||
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined
|
||||
})
|
||||
|
||||
const canTakeAction = computed(
|
||||
() => !isTransitioning.value && !options?.disabled?.value && busyReasons.value.length === 0,
|
||||
const canTakeAction = computed(() => !isTransitioning.value && !isBlockedByPropsOrBusy.value)
|
||||
|
||||
const canKill = computed(
|
||||
() =>
|
||||
!isBlockedByPropsOrBusy.value && (isStopping.value || isRunning.value || isStarting.value),
|
||||
)
|
||||
|
||||
const primaryActionText = computed(() => {
|
||||
@@ -57,7 +66,11 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
|
||||
}
|
||||
|
||||
function initiateAction(action: PowerAction) {
|
||||
if (!canTakeAction.value) return
|
||||
if (action === 'Kill') {
|
||||
if (!canKill.value) return
|
||||
} else {
|
||||
if (!canTakeAction.value) return
|
||||
}
|
||||
void sendPowerAction(action)
|
||||
}
|
||||
|
||||
@@ -70,9 +83,11 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
|
||||
isRunning,
|
||||
isStopping,
|
||||
isTransitioning,
|
||||
showStopButton,
|
||||
showStopSplit,
|
||||
showRestartButton,
|
||||
busyTooltip,
|
||||
canTakeAction,
|
||||
canKill,
|
||||
primaryActionText,
|
||||
sendPowerAction,
|
||||
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