Sfoglia il codice sorgente

add hide/show completed tasks

Aneurin Barker Snook 1 anno fa
parent
commit
2e06bbc401

+ 11 - 3
src/task/api.ts

@@ -171,25 +171,33 @@ export function searchTasks({ model }: Context): AuthRequestHandler {
 
 
     // Read parameters
     // Read parameters
     const herd = req.params.herd || undefined
     const herd = req.params.herd || undefined
+    const filters = query.array(req.query.filter, [])
     const limit = query.integer(req.query.limit, 1, 100) || 10
     const limit = query.integer(req.query.limit, 1, 100) || 10
     const page = query.integer(req.query.page, 1) || 1
     const page = query.integer(req.query.page, 1) || 1
     const search = query.str(req.query.search)
     const search = query.str(req.query.search)
     const sort = query.sorts(req.query.sort, ['description', 'position'], ['position', 'ASC'])
     const sort = query.sorts(req.query.sort, ['description', 'position'], ['position', 'ASC'])
 
 
-    // Build filters and skip
+    // Build filters
     const filter: Record<string, unknown> = {
     const filter: Record<string, unknown> = {
       _account: req.account._id,
       _account: req.account._id,
     }
     }
+    // Add herd filter if set.
+    // We don't need to verify access as it is implicitly asserted by the _account filter
     if (herd) {
     if (herd) {
-      // Add herd filter if set.
-      // We don't need to verify access as it is implicitly asserted by the _account filter
       try {
       try {
         filter._herd = new ObjectId(herd)
         filter._herd = new ObjectId(herd)
       } catch (err) {
       } catch (err) {
         return http.badRequest(res, next, { reason: 'invalid herd' })
         return http.badRequest(res, next, { reason: 'invalid herd' })
       }
       }
     }
     }
+    // Hide completed tasks by default
+    if (!filters.includes('showCompleted')) {
+      filter.done = { $eq: false }
+    }
+    // Textual search
     if (search) filter.$text = { $search: search }
     if (search) filter.$text = { $search: search }
+
+    // Set skip
     const skip = (page - 1) * limit
     const skip = (page - 1) * limit
 
 
     try {
     try {

+ 6 - 0
web/src/api/lib.ts

@@ -70,6 +70,7 @@ export class RequestError extends Error {
 
 
 /** Search and pagination parameters. */
 /** Search and pagination parameters. */
 export interface SearchParams {
 export interface SearchParams {
+  filter?: string[]
   limit?: number
   limit?: number
   page?: number
   page?: number
   search?: string
   search?: string
@@ -102,6 +103,7 @@ export function readSearchParams(up: URLSearchParams): SearchParams {
   if (up.has('page')) params.page = parseInt(up.get('page') as string)
   if (up.has('page')) params.page = parseInt(up.get('page') as string)
   if (up.has('search')) params.search = up.get('search') as string
   if (up.has('search')) params.search = up.get('search') as string
 
 
+  if (up.has('filter')) params.filter = up.getAll('filter')
   if (up.has('sort')) params.sort = up.getAll('sort')
   if (up.has('sort')) params.sort = up.getAll('sort')
 
 
   return params
   return params
@@ -153,6 +155,10 @@ export function writeSearchParams(params: SearchParams): URLSearchParams {
   if (params.page !== undefined) up.append('page', `${params.page}`)
   if (params.page !== undefined) up.append('page', `${params.page}`)
   if (params.search !== undefined) up.append('search', params.search)
   if (params.search !== undefined) up.append('search', params.search)
 
 
+  if (params.filter !== undefined) {
+    for (const filter of params.filter) up.append('filter', filter)
+  }
+
   if (params.sort !== undefined) {
   if (params.sort !== undefined) {
     for (const sort of params.sort) up.append('sort', sort)
     for (const sort of params.sort) up.append('sort', sort)
   }
   }

+ 3 - 2
web/src/components/SearchForm.tsx

@@ -1,15 +1,15 @@
 import './SearchForm.scss'
 import './SearchForm.scss'
 import Button from './button/Button'
 import Button from './button/Button'
 import ButtonSet from './ButtonSet'
 import ButtonSet from './ButtonSet'
-import type { FormEvent } from 'react'
 import FormGroup from './form/FormGroup'
 import FormGroup from './form/FormGroup'
 import FormInput from './form/FormInput'
 import FormInput from './form/FormInput'
 import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
 import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
 import Row from './Row'
 import Row from './Row'
 import { useRouteSearch } from '@/hooks'
 import { useRouteSearch } from '@/hooks'
 import { useState } from 'react'
 import { useState } from 'react'
+import type { FormEvent, PropsWithChildren } from 'react'
 
 
-export default function SearchForm() {
+export default function SearchForm({ children }: PropsWithChildren) {
   const { searchTerms, setSearchTerms } = useRouteSearch()
   const { searchTerms, setSearchTerms } = useRouteSearch()
 
 
   const [searchInput, setSearchInput] = useState(searchTerms)
   const [searchInput, setSearchInput] = useState(searchTerms)
@@ -38,6 +38,7 @@ export default function SearchForm() {
             </ButtonSet>
             </ButtonSet>
           </Row>
           </Row>
         </FormInput>
         </FormInput>
+        {children}
       </FormGroup>
       </FormGroup>
     </form>
     </form>
   )
   )

+ 13 - 0
web/src/components/form/FormToggle.scss

@@ -0,0 +1,13 @@
+@import '@/vars.scss';
+@import './mixins.scss';
+
+.toggle {
+  display: flex;
+  flex-direction: row;
+  font: inherit;
+
+  label {
+    margin-left: $space-s;
+    margin-bottom: $space-xs;
+  }
+}

+ 18 - 0
web/src/components/form/FormToggle.tsx

@@ -0,0 +1,18 @@
+import './FormToggle.scss'
+import type { HTMLAttributes } from 'react'
+
+export interface FormToggleProps extends HTMLAttributes<HTMLInputElement> {
+  checked?: boolean
+  label: string
+}
+
+export default function FormToggle({ className = '', id, label, ...props }: FormToggleProps) {
+  return (
+    <section className={`toggle ${className}`}>
+      <div className="checkbox">
+        <input type="checkbox" id={id} {...props} />
+      </div>
+      <label htmlFor={id}>{label}</label>
+    </section>
+  )
+}

+ 11 - 1
web/src/hooks/routeSearch.ts

@@ -5,6 +5,14 @@ import { useSearchParams } from 'react-router-dom'
 export function useRouteSearch() {
 export function useRouteSearch() {
   const [search, setSearch] = useSearchParams()
   const [search, setSearch] = useSearchParams()
 
 
+  function setFilters(filters: string[]) {
+    setSearch(prev => {
+      prev.delete('filter')
+      for (const filter of filters) prev.append('filter', filter)
+      return prev
+    })
+  }
+
   function setLimit(limit: number) {
   function setLimit(limit: number) {
     setSearch(prev => {
     setSearch(prev => {
       prev.set('limit', limit.toString())
       prev.set('limit', limit.toString())
@@ -47,12 +55,14 @@ export function useRouteSearch() {
   const limit = searchParams.limit || 10
   const limit = searchParams.limit || 10
   const page = searchParams.page || 1
   const page = searchParams.page || 1
 
 
+  const filter = searchParams.filter || []
   const sort = searchParams.sort || []
   const sort = searchParams.sort || []
 
 
   return {
   return {
-    limit, page, searchTerms, sort,
+    filter, limit, page, searchTerms, sort,
     searchParams, search,
     searchParams, search,
 
 
+    setFilters,
     setLimit,
     setLimit,
     setPage,
     setPage,
     setSearch,
     setSearch,

+ 11 - 2
web/src/views/HerdView.tsx

@@ -9,6 +9,7 @@ import type { DragEndEvent } from '@dnd-kit/core'
 import EditButton from '@/components/button/EditButton'
 import EditButton from '@/components/button/EditButton'
 import FormGroup from '@/components/form/FormGroup'
 import FormGroup from '@/components/form/FormGroup'
 import FormInput from '@/components/form/FormInput'
 import FormInput from '@/components/form/FormInput'
+import FormToggle from '@/components/form/FormToggle'
 import LoadingIndicator from '@/components/LoadingIndicator'
 import LoadingIndicator from '@/components/LoadingIndicator'
 import Main from '@/components/Main'
 import Main from '@/components/Main'
 import Notice from '@/components/Notice'
 import Notice from '@/components/Notice'
@@ -63,7 +64,7 @@ export default function HerdView() {
   const navigate = useNavigate()
   const navigate = useNavigate()
   const { options } = useConnection()
   const { options } = useConnection()
   const updateForm = useTaskUpdateForm()
   const updateForm = useTaskUpdateForm()
-  const { limit, page, searchParams, setPage } = useRouteSearch()
+  const { filter, limit, page, searchParams, setFilters, setPage } = useRouteSearch()
 
 
   const [data, setData] = useState<api.GetHerdResponse>()
   const [data, setData] = useState<api.GetHerdResponse>()
   const [taskData, setTaskData] = useState<api.SearchResponse<api.GetTaskResponse>>()
   const [taskData, setTaskData] = useState<api.SearchResponse<api.GetTaskResponse>>()
@@ -74,6 +75,7 @@ export default function HerdView() {
   const [loading, setLoading] = useState(false)
   const [loading, setLoading] = useState(false)
 
 
   const disableSorting = Boolean(searchParams.search)
   const disableSorting = Boolean(searchParams.search)
+  const showCompleted = filter.includes('showCompleted')
 
 
   async function createTask(data: TaskCreateFormData) {
   async function createTask(data: TaskCreateFormData) {
     if (busy) return
     if (busy) return
@@ -155,6 +157,11 @@ export default function HerdView() {
     }
     }
   }
   }
 
 
+  function toggleShowCompleted() {
+    if (showCompleted) setFilters([])
+    else setFilters(['showCompleted'])
+  }
+
   async function toggleTaskDone(task: api.WithId<api.Task>) {
   async function toggleTaskDone(task: api.WithId<api.Task>) {
     try {
     try {
       setBusy(true)
       setBusy(true)
@@ -239,7 +246,9 @@ export default function HerdView() {
         </ButtonSet>
         </ButtonSet>
       </header>
       </header>
 
 
-      <SearchForm />
+      <SearchForm>
+        <FormToggle label="Show completed tasks" id="show-completed" checked={showCompleted} onChange={toggleShowCompleted} />
+      </SearchForm>
 
 
       <Notice error={error} />
       <Notice error={error} />