Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
514 changes: 440 additions & 74 deletions components/match/MatchActions.vue

Large diffs are not rendered by default.

42 changes: 17 additions & 25 deletions components/match/MatchAdminBottomBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
Zap,
} from "lucide-vue-next";
import { useVetoOverride } from "~/composables/useVetoOverride";
import MatchActions from "~/components/match/MatchActions.vue";
import MatchAdminSteps from "~/components/match/MatchAdminSteps.vue";
import MatchServerRebootControl from "~/components/match/MatchServerRebootControl.vue";
import RconCommander from "~/components/servers/RconCommander.vue";
import ServiceLogs from "~/components/ServiceLogs.vue";
Expand Down Expand Up @@ -241,15 +243,14 @@ function runCommand(
/>
</div>

<!-- Split into three regions so the override toggle can sit between the
status and the expander without nesting a button inside a button.
The two expander halves shade off the shared `group` rather than
their own :hover, otherwise each lights up as its own patch. -->
<!-- Three regions: where the match is (ADMIN + steps), what to do now
(Veto Override and the match actions), and the console expander.
Clicking the first region also opens the console; the chevron is the
labeled control, since the steps carry their own tooltips. All three
shade off the shared `group` rather than their own :hover. -->
<div class="group flex items-stretch" :class="dragging && 'select-none'">
<button
type="button"
class="flex items-center gap-3 min-w-0 flex-1 pl-4 pr-2 py-2 group-hover:bg-muted/40 transition-colors duration-200 ease-out text-left"
:aria-expanded="open"
<div
class="flex items-center gap-3 min-w-0 flex-1 cursor-pointer pl-4 pr-2 py-2 group-hover:bg-muted/40 transition-colors duration-200 ease-out"
@click="open = !open"
>
<div
Expand All @@ -261,26 +262,17 @@ function runCommand(
"
>
<Shield class="w-3 h-3" />
{{ $t("match.admin") }}
<span class="sr-only sm:not-sr-only">{{ $t("match.admin") }}</span>
</div>

<span
v-if="canSendRCONCommands"
class="truncate font-mono text-[0.65rem] tracking-[0.18em] uppercase text-muted-foreground"
>
{{
match.is_server_online
? $t("common.connected")
: $t("common.disconnected")
}}
</span>
</button>
<MatchAdminSteps :match="match" />
</div>

<div
v-if="isVetoing"
class="flex shrink-0 items-center px-2 group-hover:bg-muted/40 transition-colors duration-200 ease-out"
class="flex shrink-0 items-center gap-2 px-2 group-hover:bg-muted/40 transition-colors duration-200 ease-out empty:hidden"
>
<button
v-if="isVetoing"
type="button"
class="inline-flex items-center gap-2 whitespace-nowrap rounded-md border px-2.5 py-1 font-mono text-[0.62rem] font-bold uppercase tracking-[0.18em] transition-colors duration-200 ease-out"
:class="
Expand All @@ -294,17 +286,17 @@ function runCommand(
<Zap class="h-3 w-3 shrink-0" />
{{ $t("match.admin_bar.veto_override") }}
</button>

<MatchActions :match="match" dock />
</div>

<button
type="button"
class="flex shrink-0 items-center gap-1.5 pl-2 pr-4 py-2 text-xs text-muted-foreground group-hover:bg-muted/40 transition-colors duration-200 ease-out"
:aria-expanded="open"
:aria-label="open ? $t('common.close') : $t('common.more')"
@click="open = !open"
>
<span class="hidden sm:inline">{{
open ? $t("common.close") : $t("common.more")
}}</span>
<ChevronUp
class="w-4 h-4 transition-transform duration-300 ease-out"
:class="open ? 'rotate-180' : 'rotate-0'"
Expand Down
117 changes: 117 additions & 0 deletions components/match/MatchAdminSteps.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<script setup lang="ts">
import { computed } from "vue";
import { Check } from "lucide-vue-next";
import FiveStackToolTip from "~/components/FiveStackToolTip.vue";
import { useAuthStore } from "~/stores/AuthStore";
import { e_match_map_status_enum, e_match_status_enum } from "~/generated/zeus";
import {
type MatchAdminStep,
currentMatchAdminStep,
matchAdminSteps,
upcomingAdminActions,
} from "~/utilities/matchAdminSteps";

const props = defineProps<{
match: Record<string, any>;
}>();

const steps = computed(() => matchAdminSteps(props.match));
const currentIndex = computed(() =>
steps.value.indexOf(currentMatchAdminStep(props.match)),
);

const upcoming = computed(() =>
upcomingAdminActions(props.match, (role) => useAuthStore().isRoleAbove(role)),
);

function labelKey(step: MatchAdminStep) {
if (
step === "live" &&
props.match.match_maps?.find((map: any) => map.is_current_map)?.status ===
e_match_map_status_enum.Paused
) {
return "match.admin_bar.steps.paused";
}
if (
step === "finished" &&
props.match.status === e_match_status_enum.Canceled
) {
return "match.admin_bar.steps.canceled";
}
return `match.admin_bar.steps.${step}`;
}

function unlocksAt(step: MatchAdminStep) {
return upcoming.value.filter((action) => action.step === step);
}
</script>

<template>
<ol
class="flex min-w-0 items-center"
:aria-label="$t('match.admin_bar.steps.label')"
>
<li v-for="(step, index) of steps" :key="step" class="flex items-center">
<span
v-if="index > 0"
aria-hidden="true"
class="mx-1 h-px w-2 sm:w-3"
:class="index <= currentIndex ? 'bg-muted-foreground/60' : 'bg-border'"
></span>
<FiveStackToolTip as-child side="top" :delay-duration="120">
<template #trigger>
<span
tabindex="0"
class="inline-flex items-center gap-1.5 rounded px-1 py-0.5 font-mono text-[0.6rem] font-bold uppercase tracking-[0.16em] outline-none focus-visible:ring-1 focus-visible:ring-[hsl(var(--tac-amber))]"
:class="
index === currentIndex
? 'text-foreground'
: index < currentIndex
? 'text-muted-foreground'
: 'text-muted-foreground/50'
"
:aria-current="index === currentIndex ? 'step' : undefined"
>
<span
class="grid size-2.5 shrink-0 place-items-center rounded-full border"
:class="
index === currentIndex
? 'border-[hsl(var(--tac-amber))] bg-[hsl(var(--tac-amber))] shadow-[0_0_0_3px_hsl(var(--tac-amber)/0.18)]'
: index < currentIndex
? 'border-muted-foreground bg-muted-foreground text-background'
: 'border-current'
"
>
<Check v-if="index < currentIndex" class="size-2" />
</span>
<span :class="index !== currentIndex && 'sr-only md:not-sr-only'">
{{ $t(labelKey(step)) }}
</span>
</span>
</template>

<template v-if="index < currentIndex">
{{ $t("match.admin_bar.steps.done") }}
</template>
<template v-else-if="index === currentIndex">
{{ $t("match.admin_bar.steps.now") }}
</template>
<template v-else-if="unlocksAt(step).length">
<span class="block font-semibold">
{{ $t("match.admin_bar.steps.unlocks") }}
</span>
<span
v-for="action of unlocksAt(step)"
:key="action.key"
class="block"
>
{{ $t(action.label) }}
</span>
</template>
<template v-else>
{{ $t("match.admin_bar.steps.nothing_new") }}
</template>
</FiveStackToolTip>
</li>
</ol>
</template>
79 changes: 33 additions & 46 deletions components/match/MatchSelectServer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import SettingHeader from "~/components/match/SettingHeader.vue";
</script>

<template>
<form class="space-y-8">
<FormField v-slot="{ componentField }" name="server_id">
<FormItem class="space-y-1.5">
<SettingHeader>{{ $t("match.server.assign") }}</SettingHeader>
<Select v-bind="componentField" @update:modelValue="updateMatchServer">
<Select v-bind="componentField" @update:modelValue="pick">
<FormControl>
<SelectTrigger>
<SelectTrigger :aria-label="$t('match.server.assign')">
<SelectValue :placeholder="$t('match.server.select')" />
</SelectTrigger>
</FormControl>
Expand All @@ -42,11 +40,11 @@ import SettingHeader from "~/components/match/SettingHeader.vue";
<SelectGroup>
<SelectLabel>{{ $t("match.server.on_demand") }}</SelectLabel>
<SelectItem
v-for="region in regions"
v-for="region in regionOptions"
:key="region.value"
:value="`0:${region.value}`"
:value="region.value"
>
{{ region.description || region.value }}
{{ region.display }}
</SelectItem>
</SelectGroup>
</SelectContent>
Expand All @@ -62,8 +60,6 @@ import * as z from "zod";
import { useForm } from "vee-validate";
import { toTypedSchema } from "~/utilities/vee-validate-zod";
import { typedGql } from "~/generated/zeus/typedDocumentNode";
import { generateMutation } from "~/graphql/graphqlGen";
import { toast } from "@/components/ui/toast";
import { useApplicationSettingsStore } from "~/stores/ApplicationSettings";
import { e_server_types_enum } from "~/generated/zeus";

Expand All @@ -74,6 +70,8 @@ export default {
required: true,
},
},
// The parent's dialog assigns the server; this only reports the pick.
emits: ["select"],
apollo: {
$subscribe: {
servers: {
Expand Down Expand Up @@ -114,54 +112,43 @@ export default {
};
},
watch: {
match: {
// Only a real server change resets the pick; the match object itself is
// replaced on every live update.
serverKey: {
immediate: true,
handler(match) {
if (match) {
let server_id = match.server_id;

if (!server_id || match.server_type === "On Demand") {
server_id = `${match.region ? `0:${match.region}` : "0"}`;
}

this.form.setValues({
server_id,
});
}
handler() {
this.syncFromMatch();
},
},
},
methods: {
async updateMatchServer() {
const [serverId, region] = this.form.values.server_id?.split(":");
syncFromMatch() {
let server_id = this.match.server_id;

await this.$apollo.mutate({
mutation: generateMutation({
update_matches_by_pk: [
{
pk_columns: {
id: this.match.id,
},
_set: {
region,
server_id: serverId === "0" ? null : this.form.values.server_id,
},
},
{
id: true,
},
],
}),
});
if (!server_id || this.match.server_type === "On Demand") {
server_id = `${this.match.region ? `0:${this.match.region}` : "0"}`;
}

toast({
title: this.$t("match.server.assigned"),
this.form.setValues({
server_id,
});
},
pick(value: string) {
const option = [...this.availableServers, ...this.regionOptions].find(
(option) => option.value === value,
);
this.$emit("select", { value, label: option?.display ?? value });
},
},
computed: {
regions() {
return useApplicationSettingsStore().availableRegions;
serverKey() {
return `${this.match.server_id}:${this.match.server_type}:${this.match.region}`;
},
regionOptions() {
return useApplicationSettingsStore().availableRegions.map((region) => ({
value: `0:${region.value}`,
display: region.description || region.value,
}));
},
canSelectDedicatedServer() {
const { isAdmin, isMatchOrganizer, isTournamentOrganizer } =
Expand Down
Loading
Loading