feat: add persisted List/Board view toggle to Chorus
This commit is contained in:
parent
cee0dcd36a
commit
18cfd22db5
2 changed files with 110 additions and 7 deletions
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import TriageList from './components/TriageList.vue'
|
||||
import KanbanBoard from './components/KanbanBoard.vue'
|
||||
import ItemModal from './components/ItemModal.vue'
|
||||
import { fetchItems, fetchItem } from './api.js'
|
||||
|
||||
|
|
@ -9,6 +10,14 @@ const selectedItem = ref(null)
|
|||
const includeDone = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const VIEW_MODE_KEY = 'chorus_view_mode'
|
||||
const storedViewMode = localStorage.getItem(VIEW_MODE_KEY)
|
||||
const viewMode = ref(storedViewMode === 'board' ? 'board' : 'list')
|
||||
|
||||
watch(viewMode, (mode) => {
|
||||
localStorage.setItem(VIEW_MODE_KEY, mode)
|
||||
})
|
||||
|
||||
async function loadItems() {
|
||||
try {
|
||||
errorMessage.value = ''
|
||||
|
|
@ -49,10 +58,29 @@ onMounted(async () => {
|
|||
<div class="container">
|
||||
<h1>Chorus</h1>
|
||||
|
||||
<label class="include-done-toggle">
|
||||
<input type="checkbox" v-model="includeDone" @change="loadItems" />
|
||||
Show completed
|
||||
</label>
|
||||
<div class="view-controls">
|
||||
<label class="include-done-toggle">
|
||||
<input type="checkbox" v-model="includeDone" @change="loadItems" />
|
||||
Show completed
|
||||
</label>
|
||||
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: viewMode === 'list' }"
|
||||
@click="viewMode = 'list'"
|
||||
>
|
||||
List
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: viewMode === 'board' }"
|
||||
@click="viewMode = 'board'"
|
||||
>
|
||||
Board
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="status-message">{{ errorMessage }}</p>
|
||||
|
||||
|
|
@ -60,22 +88,52 @@ onMounted(async () => {
|
|||
Nothing to triage right now.
|
||||
</p>
|
||||
|
||||
<TriageList v-else :items="items" @select-item="openItem" />
|
||||
<TriageList v-else-if="viewMode === 'list'" :items="items" @select-item="openItem" />
|
||||
<KanbanBoard v-else :items="items" @select-item="openItem" />
|
||||
|
||||
<ItemModal :item="selectedItem" @close="closeModal" @saved="onSaved" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.view-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.include-done-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.view-toggle button {
|
||||
min-height: 44px;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-toggle button.active {
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.status-message {
|
||||
color: var(--color-text-muted);
|
||||
padding: var(--spacing-md);
|
||||
|
|
|
|||
45
frontend/tests/App.spec.js
Normal file
45
frontend/tests/App.spec.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import App from '../src/App.vue'
|
||||
|
||||
vi.mock('../src/api.js', () => ({
|
||||
fetchItems: vi.fn(async () => []),
|
||||
fetchItem: vi.fn(async (id) => ({ id })),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('App view mode', () => {
|
||||
it('defaults to list view when nothing is stored', async () => {
|
||||
const wrapper = mount(App)
|
||||
await flushPromises()
|
||||
const listButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'List')
|
||||
expect(listButton.classes()).toContain('active')
|
||||
})
|
||||
|
||||
it('honors a stored board view mode on mount', async () => {
|
||||
localStorage.setItem('chorus_view_mode', 'board')
|
||||
const wrapper = mount(App)
|
||||
await flushPromises()
|
||||
const boardButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'Board')
|
||||
expect(boardButton.classes()).toContain('active')
|
||||
})
|
||||
|
||||
it('defaults to list view for an invalid stored value', async () => {
|
||||
localStorage.setItem('chorus_view_mode', 'garbage')
|
||||
const wrapper = mount(App)
|
||||
await flushPromises()
|
||||
const listButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'List')
|
||||
expect(listButton.classes()).toContain('active')
|
||||
})
|
||||
|
||||
it('clicking Board writes the choice to localStorage', async () => {
|
||||
const wrapper = mount(App)
|
||||
await flushPromises()
|
||||
const boardButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'Board')
|
||||
await boardButton.trigger('click')
|
||||
expect(localStorage.getItem('chorus_view_mode')).toBe('board')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue