Skip to content

Commit

Permalink
feature: save column selections in cookie
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Carlson committed Aug 28, 2024
1 parent 02f51f0 commit b5f759e
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 22 deletions.
1 change: 1 addition & 0 deletions packages/app/components/search/search-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function getColumns(modelName: string): ColumnDef<Document>[] {
),
},
{
id: 'state',
accessorKey: 'x-meditor.state',
filterFn: 'includesString',
header: ({ column }) => {
Expand Down
88 changes: 75 additions & 13 deletions packages/app/components/search/search-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,36 @@ import {
getSortedRowModel,
Row,
SortingState,
Table,
useReactTable,
VisibilityState,
} from '@tanstack/react-table'

import { SearchPagination } from './search-pagination'
import { useContext, useMemo, useState } from 'react'
import { Button, Dropdown } from 'react-bootstrap'
import { Button, Dropdown, Form } from 'react-bootstrap'
import { FaEye, FaFilter, FaTrash, FaWrench } from 'react-icons/fa'
import { MdAdd } from 'react-icons/md'
import { LuFileSpreadsheet, LuFileJson } from 'react-icons/lu'
import { download, generateCsv, mkConfig } from 'export-to-csv'
import { Document } from '@/documents/types'
import { AppContext } from '../app-store'
import { ModelWithWorkflow } from '@/models/types'
import { flattenSchema } from '@/utils/jsonschema'
import { DropdownList } from '../dropdown-list'
import { xMeditorSchema } from '@/models/constants'
import { Document } from '@/documents/types'
import { useRouter } from 'next/router'
import { useCookie } from '@/lib/use-cookie.hook'

function findColumnByKey(table: Table<any>, key: string) {
return (
table
.getAllColumns()
// the schema key is dot-separated `.` whereas the TanStack table uses `_`
// we'll convert to `_` to make sure we match correctly
.find(c => c.id === key.replace(/\./g, '_'))
)
}

interface SearchTableProps<TData, TValue> {
model: ModelWithWorkflow
Expand All @@ -42,12 +57,23 @@ export function SearchTable<Document, TValue>({
globalFilter,
onGlobalFilterChange,
}: SearchTableProps<Document, TValue>) {
const router = useRouter()
const flattenedSchema = useMemo(() => {
return flattenSchema(JSON.parse(model.schema))
const schema = JSON.parse(model.schema)

return flattenSchema({
...schema,
properties: {
...xMeditorSchema, // add x-meditor fields
...schema.properties,
},
})
}, [model.schema])

const { setSuccessNotification } = useContext(AppContext)
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [showColumnsDropdown, setShowColumnsDropdown] = useState(false)
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [sorting, setSorting] = useState<SortingState>([
{
// default sorting
Expand All @@ -56,6 +82,10 @@ export function SearchTable<Document, TValue>({
},
])

const [includeColumns, setIncludeColumns] = useCookie<{
[key: string]: string[]
}>('includeColumns', {})

const table = useReactTable<Document>({
data, // each row of data (i.e. an array of documents)
columns, // column definitions that describe how each column should function (sort, filter, etc.)
Expand All @@ -65,12 +95,9 @@ export function SearchTable<Document, TValue>({
sorting,
globalFilter,
columnFilters,
columnVisibility,
},

debugTable: true,
debugHeaders: true,
debugColumns: true,

// support for pagination (https://tanstack.com/table/v8/docs/guide/pagination)
getPaginationRowModel: getPaginationRowModel(),

Expand All @@ -85,6 +112,9 @@ export function SearchTable<Document, TValue>({
getFacetedUniqueValues: getFacetedUniqueValues(), // generate unique values for select filter/autocomplete
getFacetedMinMaxValues: getFacetedMinMaxValues(), // generate min/max values for range filter

// support for toggling visibility of columns
onColumnVisibilityChange: setColumnVisibility,

// support for global search/filter (https://tanstack.com/table/v8/docs/guide/global-filtering)
onGlobalFilterChange: onGlobalFilterChange,
globalFilterFn: 'includesString',
Expand Down Expand Up @@ -124,8 +154,22 @@ export function SearchTable<Document, TValue>({
setSuccessNotification(`Exported ${rows.length} rows to ${model.name}.json`)
}

const toggleVisibleColumn = (key: string) => {
console.log('toggle visible column ', key)
const toggleVisibleColumn = async (key: string, visible: boolean) => {
const column = findColumnByKey(table, key)

if (!column) {
// we don't have this column yet, add it to local storage, refresh the page
setIncludeColumns({
...includeColumns,
[model.name]: includeColumns[model.name]
? [...includeColumns[model.name], key]
: [key],
})

router.reload()
}

column?.toggleVisibility(visible)
}

/*
Expand Down Expand Up @@ -214,16 +258,34 @@ export function SearchTable<Document, TValue>({
Columns
</Dropdown.Toggle>

<Dropdown.Menu as={DropdownList}>
<Dropdown.Menu
as={DropdownList}
style={{ maxWidth: '300px' }}
>
<Dropdown.Header>
Select visible columns
</Dropdown.Header>

{flattenedSchema.map(item => {
return (
<a href="javascript:;" key={item.key}>
{item.key}
</a>
<Form.Check
key={item.key}
type="checkbox"
label={item.key}
className="py-1 px-4 ml-3"
checked={
findColumnByKey(
table,
item.key
)?.getIsVisible() ?? false
}
onChange={e =>
toggleVisibleColumn(
item.key,
!!e.target.checked
)
}
/>
)
})}
</Dropdown.Menu>
Expand Down
22 changes: 14 additions & 8 deletions packages/app/documents/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,19 @@ class DocumentsDb {
const sortDir =
(searchOptions?.sort || this.#DEFAULT_SORT).charAt(0) == '-' ? -1 : 1

// since documents can be so large, only include a handful of needed fields
const $project = {
_id: 0,
title: `$${titleProperty}`, // add a title field that matches the `titleProperty` field
[titleProperty]: 1,
'x-meditor': 1,
}

// dynamically populate which fields we'll include
searchOptions?.includeFields?.forEach(field => {
$project[field] = 1
})

const pipeline = [
// filter out deleted documents
{
Expand All @@ -296,15 +309,8 @@ class DocumentsDb {
},
},

// since documents can be so large, only include a handful of needed fields
// TODO: once pagination is added to the API, this shouldn't be needed anymore
{
$project: {
_id: 0,
title: `$${titleProperty}`, // add a title field that matches the `titleProperty` field
[titleProperty]: 1,
'x-meditor': 1,
},
$project,
},

// make sure we only return the latest version of each document (collection holds document history)
Expand Down
2 changes: 1 addition & 1 deletion packages/app/documents/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { getModel, getModelWithWorkflow } from '../models/service'
import type { DocumentsSearchOptions, ModelWithWorkflow } from '../models/types'
import { publishMessageToQueueChannel } from '../publication-queue/service'
import { ErrorCause, ErrorCode, HttpException } from '../utils/errors'
import { formatValidationErrorMessage } from '../utils/jsonschema-validate'
import { formatValidationErrorMessage } from '../utils/jsonschema'
import { getTargetStatesFromWorkflow } from '../workflows/service'
import type { Workflow, WorkflowEdge } from '../workflows/types'
import { getDocumentsDb } from './db'
Expand Down
24 changes: 24 additions & 0 deletions packages/app/lib/use-cookie.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getCookie, setCookie } from 'cookies-next'
import { useState } from 'react'

export const useCookie = <V>(
key: string,
initialValue: V
): [V, (value: Function | any) => void] => {
const [storedValue, setStoredValue] = useState(() => {
const item = getCookie(key)
return item ? JSON.parse(item.toString()) : initialValue
})

const setValue = value => {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
setCookie(key, JSON.stringify(valueToStore))
}

if (typeof document === 'undefined') {
return [null, null]
}

return [storedValue, setValue]
}
12 changes: 12 additions & 0 deletions packages/app/models/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// a small JSONSchema for validating the `x-meditor` fields in a document
export const xMeditorSchema = {
state: {
type: 'string',
},
modifiedBy: {
type: 'string',
},
modifiedOn: {
type: 'string',
},
}
1 change: 1 addition & 0 deletions packages/app/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface DocumentsSearchOptions {
searchTerm?: string
filter?: string // ex. state:Draft
sort?: string // ex. modifiedOn | -modifiedOn
includeFields?: string[] // ex. ["Property", "Property.ChildItem"]
}

export interface ModelCategory {
Expand Down
6 changes: 6 additions & 0 deletions packages/app/pages/[modelName]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { User } from '../../auth/types'
import { isNotFoundError } from 'utils/errors'
import { SearchTable } from '@/components/search/search-table'
import { getColumns } from '@/components/search/search-columns'
import { getCookie } from 'cookies-next'

type ModelPageProps = {
user: User
Expand Down Expand Up @@ -88,6 +89,11 @@ export async function getServerSideProps(ctx: NextPageContext) {
}
}

// if user has preferences for adding additional columns to the view, include them here
const includeColumns = JSON.parse(
getCookie('includeColumns', ctx)?.toString() ?? ''
)

// fetch documents, applying search, filter, or sort
const [documentsError, documents] = await getDocumentsForModel(modelName)

Expand Down
3 changes: 3 additions & 0 deletions packages/app/pages/api/models/[modelName]/documents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
...(req.query.searchTerm && {
searchTerm: req.query.searchTerm.toString(),
}),
...(req.query.includeFields && {
fields: req.query.includeFields.toString().split(','),
}),
})

if (error) {
Expand Down

0 comments on commit b5f759e

Please sign in to comment.