Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 0 additions & 43 deletions src/components/builder/BuilderExitButton.vue

This file was deleted.

151 changes: 151 additions & 0 deletions src/components/builder/BuilderFooterToolbar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, ref } from 'vue'
import { createI18n } from 'vue-i18n'

import type { AppMode } from '@/composables/useAppMode'

import BuilderFooterToolbar from '@/components/builder/BuilderFooterToolbar.vue'

const mockSetMode = vi.hoisted(() => vi.fn())
const mockExitBuilder = vi.hoisted(() => vi.fn())
const mockShowDialog = vi.hoisted(() => vi.fn())

const mockState = {
mode: 'builder:select' as AppMode,
settingView: false
}

vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({
mode: computed(() => mockState.mode),
isBuilderMode: ref(true),
setMode: mockSetMode
})
}))

const mockHasOutputs = ref(true)

vi.mock('@/stores/appModeStore', () => ({
useAppModeStore: () => ({
exitBuilder: mockExitBuilder,
hasOutputs: mockHasOutputs,
$id: 'appMode'
})
}))

vi.mock('@/stores/dialogStore', () => ({
useDialogStore: () => ({
dialogStack: []
})
}))

vi.mock('@/components/builder/useAppSetDefaultView', () => ({
useAppSetDefaultView: () => ({
settingView: computed(() => mockState.settingView),
showDialog: mockShowDialog
})
}))

const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
builderMenu: { exitAppBuilder: 'Exit app builder' },
builderFooterToolbar: {
label: 'Builder navigation',
back: 'Back',
next: 'Next'
}
}
}
})

describe('BuilderFooterToolbar', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
mockState.mode = 'builder:select'
mockHasOutputs.value = true
mockState.settingView = false
})

function mountComponent() {
return mount(BuilderFooterToolbar, {
global: {
plugins: [i18n],
stubs: { Button: false }
}
})
}

function getButtons(wrapper: ReturnType<typeof mountComponent>) {
const buttons = wrapper.findAll('button')
return {
exit: buttons[0],
back: buttons[1],
next: buttons[2]
}
}

it('disables back on the first step', () => {
mockState.mode = 'builder:select'
const { back } = getButtons(mountComponent())
expect(back.attributes('disabled')).toBeDefined()
})

it('enables back on the second step', () => {
mockState.mode = 'builder:arrange'
const { back } = getButtons(mountComponent())
expect(back.attributes('disabled')).toBeUndefined()
})

it('disables next on the setDefaultView step', () => {
mockState.settingView = true
const { next } = getButtons(mountComponent())
expect(next.attributes('disabled')).toBeDefined()
})

it('disables next on arrange step when no outputs', () => {
mockState.mode = 'builder:arrange'
mockHasOutputs.value = false
const { next } = getButtons(mountComponent())
expect(next.attributes('disabled')).toBeDefined()
})

it('enables next on select step', () => {
mockState.mode = 'builder:select'
const { next } = getButtons(mountComponent())
expect(next.attributes('disabled')).toBeUndefined()
})

it('calls setMode on back click', async () => {
mockState.mode = 'builder:arrange'
const { back } = getButtons(mountComponent())
await back.trigger('click')
expect(mockSetMode).toHaveBeenCalledWith('builder:select')
})

it('calls setMode on next click from select step', async () => {
mockState.mode = 'builder:select'
const { next } = getButtons(mountComponent())
await next.trigger('click')
expect(mockSetMode).toHaveBeenCalledWith('builder:arrange')
})

it('opens default view dialog on next click from arrange step', async () => {
mockState.mode = 'builder:arrange'
const { next } = getButtons(mountComponent())
await next.trigger('click')
expect(mockSetMode).toHaveBeenCalledWith('builder:arrange')
expect(mockShowDialog).toHaveBeenCalledOnce()
})

it('calls exitBuilder on exit button click', async () => {
const { exit } = getButtons(mountComponent())
await exit.trigger('click')
expect(mockExitBuilder).toHaveBeenCalledOnce()
})
})
86 changes: 86 additions & 0 deletions src/components/builder/BuilderFooterToolbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<nav
class="fixed bottom-4 left-1/2 z-1000 flex -translate-x-1/2 items-center gap-2 rounded-2xl border border-border-default bg-base-background p-2 shadow-interface"
:aria-label="t('builderFooterToolbar.label')"
>
<Button variant="textonly" size="lg" @click="onExitBuilder">
{{ t('builderMenu.exitAppBuilder') }}
</Button>
<Button
variant="textonly"
size="lg"
:disabled="isFirstStep"
@click="onBack"
>
<i class="icon-[lucide--chevron-left]" aria-hidden="true" />
{{ t('builderFooterToolbar.back') }}
</Button>
<Button size="lg" :disabled="isLastStep" @click="onNext">
{{ t('builderFooterToolbar.next') }}
<i class="icon-[lucide--chevron-right]" aria-hidden="true" />
</Button>
</nav>
</template>

<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'

import Button from '@/components/ui/button/Button.vue'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import { useDialogStore } from '@/stores/dialogStore'

import { useAppSetDefaultView } from './useAppSetDefaultView'
import type { BuilderStepId } from './useBuilderSteps'
import { BUILDER_STEPS, useBuilderSteps } from './useBuilderSteps'

const { t } = useI18n()
const appModeStore = useAppModeStore()
const dialogStore = useDialogStore()
const { isBuilderMode, setMode } = useAppMode()
const { hasOutputs } = storeToRefs(appModeStore)
const { showDialog } = useAppSetDefaultView()
const { activeStepIndex, isFirstStep, isLastStep } = useBuilderSteps({
hasOutputs
})

useEventListener(window, 'keydown', (e: KeyboardEvent) => {
if (
e.key === 'Escape' &&
!e.ctrlKey &&
!e.altKey &&
!e.metaKey &&
dialogStore.dialogStack.length === 0 &&
isBuilderMode.value
) {
e.preventDefault()
e.stopPropagation()
onExitBuilder()
}
Comment thread
pythongosssss marked this conversation as resolved.
})

function onExitBuilder() {
void appModeStore.exitBuilder()
}

function navigateToStep(stepId: BuilderStepId) {
if (stepId === 'setDefaultView') {
setMode('builder:arrange')
showDialog()
} else {
setMode(stepId)
}
}

function onBack() {
if (isFirstStep.value) return
navigateToStep(BUILDER_STEPS[activeStepIndex.value - 1])
}

function onNext() {
if (isLastStep.value) return
navigateToStep(BUILDER_STEPS[activeStepIndex.value + 1])
}
</script>
14 changes: 6 additions & 8 deletions src/components/builder/BuilderToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

import { useAppMode } from '@/composables/useAppMode'
Expand All @@ -82,15 +81,14 @@ import StepBadge from './StepBadge.vue'
import StepLabel from './StepLabel.vue'
import type { BuilderToolbarStep } from './types'
import { useAppSetDefaultView } from './useAppSetDefaultView'
import { useBuilderSteps } from './useBuilderSteps'

const { t } = useI18n()
const { mode, setMode } = useAppMode()
const { hasOutputs } = storeToRefs(useAppModeStore())
const { settingView, showDialog } = useAppSetDefaultView()

const activeStep = computed(() =>
settingView.value ? 'setDefaultView' : mode.value
)
const { setMode } = useAppMode()
const appModeStore = useAppModeStore()
const { hasOutputs } = storeToRefs(appModeStore)
const { showDialog } = useAppSetDefaultView()
const { activeStep } = useBuilderSteps()

const stepClasses =
'inline-flex h-14 min-h-8 cursor-pointer items-center gap-3 rounded-lg py-2 pr-4 pl-2 transition-colors border-none'
Expand Down
44 changes: 44 additions & 0 deletions src/components/builder/useBuilderSteps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Ref } from 'vue'

import { computed } from 'vue'

import { useAppMode } from '@/composables/useAppMode'

import { useAppSetDefaultView } from './useAppSetDefaultView'

export const BUILDER_STEPS = [
'builder:select',
'builder:arrange',
'setDefaultView'
] as const

export type BuilderStepId = (typeof BUILDER_STEPS)[number]

const ARRANGE_INDEX = BUILDER_STEPS.indexOf('builder:arrange')

export function useBuilderSteps(options?: { hasOutputs?: Ref<boolean> }) {
const { mode, isBuilderMode } = useAppMode()
const { settingView } = useAppSetDefaultView()

const activeStep = computed<BuilderStepId>(() => {
if (settingView.value) return 'setDefaultView'
if (isBuilderMode.value) {
return mode.value as BuilderStepId
}
return 'builder:select'
})

const activeStepIndex = computed(() =>
BUILDER_STEPS.indexOf(activeStep.value)
)
Comment thread
pythongosssss marked this conversation as resolved.

const isFirstStep = computed(() => activeStepIndex.value === 0)

const isLastStep = computed(() => {
if (!options?.hasOutputs?.value)
return activeStepIndex.value >= ARRANGE_INDEX
return activeStepIndex.value >= BUILDER_STEPS.length - 1
})

return { activeStep, activeStepIndex, isFirstStep, isLastStep }
}
5 changes: 5 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -3373,5 +3373,10 @@
},
"builderMenu": {
"exitAppBuilder": "Exit app builder"
},
"builderFooterToolbar": {
"label": "Builder navigation",
"back": "Back",
"next": "Next"
}
}
4 changes: 2 additions & 2 deletions src/views/GraphView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<template v-if="isBuilderMode">
<BuilderToolbar />
<BuilderMenu />
<BuilderExitButton />
<BuilderFooterToolbar />
</template>
</div>

Expand Down Expand Up @@ -91,7 +91,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { electronAPI } from '@/utils/envUtil'
import BuilderExitButton from '@/components/builder/BuilderExitButton.vue'
import BuilderFooterToolbar from '@/components/builder/BuilderFooterToolbar.vue'
import BuilderMenu from '@/components/builder/BuilderMenu.vue'
import BuilderToolbar from '@/components/builder/BuilderToolbar.vue'
import LinearView from '@/views/LinearView.vue'
Expand Down
Loading