Skip to content

Commit

Permalink
feat(#193): refactor to extract room list logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Jumpy-Squirrel committed Nov 15, 2024
1 parent 6c05034 commit f00912e
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 71 deletions.
2 changes: 1 addition & 1 deletion src/html/common/vue/rooms/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ export const App = {
template: `
<ErrorList />
<RoomForm :id="roomId" @rooms-possibly-updated="reloadRooms"/>
<RoomList :reload="updateCount" @room-clicked="(id) => setRoomId(id)" @filter-changed="reloadRooms"/>
<RoomList :reload="updateCount" @room-clicked="(room) => setRoomId(room ? room.id : '')" @filter-changed="reloadRooms"/>
`
}
106 changes: 36 additions & 70 deletions src/html/common/vue/rooms/roomlist.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { ListAllRooms } from '../apis/roomsrv.js'
import { StoredErrorList } from '../stores/errorlist.js'
import { debug } from '../shared/debug.js'
import { useTernary } from '../use/useternary.js'
import { useRoomList } from '../use/useroomlist.js'

const { ref, watch } = Vue
const { watch } = Vue
const { useI18n } = VueI18n

export const RoomList = {
Expand All @@ -13,93 +11,61 @@ export const RoomList = {
debug('RoomList.setup', props)
const { t } = useI18n()

const roomList = ref([])
const selectedId = ref('')
const filter = ref('')
const finalFilter = useTernary(undefined)
const hcFilter = useTernary(undefined)
const list = useRoomList()

const setRoomList = (rooms) => {
debug('RoomList.setRoomList', rooms)
roomList.value = rooms
selectedId.value = ''
const roomClicked = (id) => {
debug('RoomList.roomClicked', id)
list.select(id)
emit('roomClicked', list.selected.value)
}

const emitRoomClicked = (id) => {
debug('RoomList.emitRoomClicked', id)
if (selectedId.value === id) {
// allow un-select
selectedId.value = ''
} else {
selectedId.value = id
}
emit('roomClicked', selectedId.value)
}
const emitFilterChanged = () => {
debug('RoomList.emitFilterChanged')
selectedId.value = ''
emit('filterChanged')
}

const matchesFilter = (room) => {
const matchesNameFilter = filter.value ? room.name.toLowerCase().includes(filter.value.toLowerCase()) : true
const matchesFinalFilter = finalFilter.matches((room.flags ?? []).includes('final'))
const matchesHcFilter = hcFilter.matches((room.flags ?? []).includes('handicapped'))
return matchesNameFilter && matchesFinalFilter && matchesHcFilter
}
const fetchRoomList = () => {
debug('RoomList.fetchRoomList')
ListAllRooms((rooms) => {
debug('RoomList.fetchRoomList.success', rooms)
rooms = rooms.filter(matchesFilter)
setRoomList(rooms)
}, (status, apiError) => {
debug('RoomList.fetchRoomList.error', status, apiError)
StoredErrorList.errors.addError(apiError)
})
}

const finalColumnClicked = () => {
debug('RoomList.finalColumnClicked')
finalFilter.cycle()
fetchRoomList()
emitFilterChanged()
list.filter.final.cycle()
list.apply()
emit('filterChanged')
}
const hcColumnClicked = () => {
debug('RoomList.hcColumnClicked')
hcFilter.cycle()
fetchRoomList()
emitFilterChanged()
list.filter.handicapped.cycle()
list.apply()
emit('filterChanged')
}

watch(filter, (newValue, oldValue) => {
watch(list.filter.name, (newValue, oldValue) => {
if (newValue !== oldValue) {
fetchRoomList()
emitFilterChanged()
debug('RoomList.filter.name changed', oldValue, newValue)
list.apply()
emit('filterChanged')
}
})

watch(() => props.reload, (newValue, oldValue) => {
debug('RoomList.watch props.reload', oldValue, newValue)
fetchRoomList()
list.reload()
})

const rooms = list.rooms
const selected = list.selected
const nameFilter = list.filter.name
const finalFilter = list.filter.final
const hcFilter = list.filter.handicapped

return {
t,
roomList,
selectedId,
filter,
rooms,
selected,
nameFilter,
finalFilter,
hcFilter,
roomClicked,
finalColumnClicked,
hcColumnClicked,
emitRoomClicked,
}
},
template: `
<div class="headline"><br/>{{ t('rooms.list.title') }}</div>
<hr class="contentbox"/>
<p>{{ t('rooms.list.filter') }}: <input type="text" size="40" maxlength="80" v-model.trim="filter"/></p>
<p>{{ t('rooms.list.filter') }}: <input type="text" size="40" maxlength="80" v-model.trim="nameFilter"/></p>
<p>{{ t('rooms.list.info') }}</p>
<table class="searchlist">
<tr>
Expand All @@ -110,13 +76,13 @@ export const RoomList = {
<th class="searchlist" @click="hcColumnClicked">{{ t('rooms.list.header.handicapped') }}&nbsp;{{ hcFilter.display('✔','❌','·') }}</th>
<th class="searchlist">{{ t('rooms.list.header.comments') }}</th>
</tr>
<tr v-for="(r, i) in roomList" class="searchlist_sep" @click="emitRoomClicked(r.id)">
<td :class="r.id === selectedId ? 'searchlist selected' : 'searchlist'" align="right">{{ i+1 }}</td>
<td :class="r.id === selectedId ? 'searchlist selected' : 'searchlist'">{{ r.name }}</td>
<td :class="r.id === selectedId ? 'searchlist selected' : 'searchlist'" align="right">{{ r.size }}</td>
<td :class="r.id === selectedId ? 'searchlist selected' : 'searchlist'" align="center">{{ (r.flags ?? []).includes('final') ? '✔' : '' }}</td>
<td :class="r.id === selectedId ? 'searchlist selected' : 'searchlist'" align="center">{{ (r.flags ?? []).includes('handicapped') ? '✔' : '' }}</td>
<td :class="r.id === selectedId ? 'searchlist selected' : 'searchlist'">{{ r.comments }}</td>
<tr v-for="(r, i) in rooms" class="searchlist_sep" @click="roomClicked(r.id)">
<td :class="selected && r.id === selected.id ? 'searchlist selected' : 'searchlist'" align="right">{{ i+1 }}</td>
<td :class="selected && r.id === selected.id ? 'searchlist selected' : 'searchlist'">{{ r.name }}</td>
<td :class="selected && r.id === selected.id ? 'searchlist selected' : 'searchlist'" align="right">{{ r.size }}</td>
<td :class="selected && r.id === selected.id ? 'searchlist selected' : 'searchlist'" align="center">{{ (r.flags ?? []).includes('final') ? '✔' : '' }}</td>
<td :class="selected && r.id === selected.id ? 'searchlist selected' : 'searchlist'" align="center">{{ (r.flags ?? []).includes('handicapped') ? '✔' : '' }}</td>
<td :class="selected && r.id === selected.id ? 'searchlist selected' : 'searchlist'">{{ r.comments }}</td>
</tr>
</table>
`
Expand Down
109 changes: 109 additions & 0 deletions src/html/common/vue/use/useroomlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ListAllRooms } from '../apis/roomsrv.js'
import { StoredErrorList } from '../stores/errorlist.js'
import { debug } from '../shared/debug.js'
import { useTernary } from './useternary.js'

const { ref } = Vue

// useRoomList provides a list of rooms that can be filtered and sorted.
//
// This composable integrates with the backend API and has a local cache to avoid unnecessary fetches.
//
// errors are asynchronously added to the errorlist store, but the errorlist is never cleared - you have to do that
// from the outside.
export const useRoomList = () => {
// the filtered list of rooms. Updated by fetch() or filter().
//
// Should not be updated from the outside!
const rooms = ref([])

// the currently selected room, or undefined if no room is selected.
//
// Should not be updated directly from the outside!
//
// use select() to select or de-select a room.
const selected = ref(undefined)

// the filter criteria.
//
// Maintained from outside this composable, but kept here to keep everything together.
const filter = {
name: ref(''),
final: useTernary(undefined),
handicapped: useTernary(undefined),
}

// the raw list of rooms caches the response from the backend API.
//
// Exposed but should not normally be interacted with in any way.
const rawRooms = ref([])

// reload performs an async fetch of the raw rooms list from the backend API.
//
// if any errors occur, they are added to the errorlist store.
//
// after successfully retrieving the raw list, it is also refiltered, and the selected room is cleared.
const reload = () => {
debug('useRoomList.reload')
selected.value = undefined
ListAllRooms((rms) => {
debug('useRoomList.reload.success', rms)
rawRooms.value = rms
apply()
}, (status, apiError) => {
debug('useRoomList.reload.error', status, apiError)
StoredErrorList.errors.addError(apiError)
})
}

const matchesFilter = (rm) => {
const isFinal = (rm.flags ?? []).includes('final')
const isHandicapped = (rm.flags ?? []).includes('handicapped')

const matchesNameFilter = filter.name.value ? rm.name.toLowerCase().includes(filter.name.value.toLowerCase()) : true
const matchesFinalFilter = filter.final.matches(isFinal)
const matchesHcFilter = filter.handicapped.matches(isHandicapped)
return matchesNameFilter && matchesFinalFilter && matchesHcFilter
}

// apply uses the internal cache to apply new filter settings.
//
// no API access occurs, so this is a synchronous operation.
//
// clears the selected room because it may no longer be in the list.
const apply = () => {
debug('useRoomList.refilter')
selected.value = undefined
// TODO sort not implemented
rooms.value = rawRooms.value.filter(matchesFilter)
}

// select selects a room (by id), but only if it is in the current filter.
//
// If the room id is not found in the list, the selection is cleared.
//
// If the same room is already selected, it is instead unselected.
//
// Pass undefined to clear the selection.
const select = (id) => {
debug('useRoomList.select', id)
if (id === undefined) {
debug('useRoomList.select cleared')
selected.value = undefined
} else {
const matchingRoomsInList = rooms.value.filter((rm) => rm.id === id)
if (matchingRoomsInList.length === 0) {
debug('useRoomList.select id not in list - clearing selection', id)
selected.value = undefined
} else if (selected.value && selected.value.id === id) {
debug('useRoomList.select id already selected - unselecting it', id)
selected.value = undefined
} else {
debug('useRoomList.select ok', id)
selected.value = matchingRoomsInList[0]
}
}
}

return { rooms, selected, filter, rawRooms, reload, apply, select }
}

0 comments on commit f00912e

Please sign in to comment.