diff --git a/src/blitz-client.ts b/src/blitz-client.ts
index db1b392d..33833cd8 100644
--- a/src/blitz-client.ts
+++ b/src/blitz-client.ts
@@ -7,5 +7,14 @@ export const authConfig = {
}
export const { withBlitz } = setupBlitzClient({
- plugins: [AuthClientPlugin(authConfig), BlitzRpcPlugin({})],
+ plugins: [
+ AuthClientPlugin(authConfig),
+ BlitzRpcPlugin({
+ reactQueryOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ },
+ },
+ }),
+ ],
})
diff --git a/src/core/components/DaisyTheme.tsx b/src/core/components/DaisyTheme.tsx
index df61b1fd..f5533986 100644
--- a/src/core/components/DaisyTheme.tsx
+++ b/src/core/components/DaisyTheme.tsx
@@ -15,7 +15,18 @@ import {
getUiOptions,
getSubmitButtonOptions,
SubmitButtonProps,
+ schemaRequiresTrueValue,
+ descriptionId,
+ ariaDescribedByIds,
+ enumOptionsIsSelected,
+ enumOptionsSelectValue,
+ enumOptionsDeselectValue,
+ enumOptionsValueForIndex,
+ optionId,
} from "@rjsf/utils"
+import ReactMarkdown from "react-markdown"
+import remarkGfm from "remark-gfm"
+import remarkBreaks from "remark-breaks"
import { ThemeProps } from "@rjsf/core"
@@ -74,9 +85,12 @@ function MyDescriptionField<
}
if (typeof description === "string") {
return (
-
- {description}
-
+
+ {description}
+
)
} else {
return (
@@ -186,6 +200,118 @@ const MyEmailWidget = (props: WidgetProps) => {
)
}
+const MyCheckboxWidget = (props: WidgetProps) => {
+ const {
+ id,
+ value,
+ disabled,
+ readonly,
+ label,
+ hideLabel,
+ onChange,
+ onBlur,
+ onFocus,
+ options,
+ schema,
+ uiSchema,
+ registry,
+ } = props
+ const DescriptionFieldTemplate = getTemplate("DescriptionFieldTemplate", registry, options)
+ const description = options.description ?? schema.description
+ const required = schemaRequiresTrueValue(schema)
+
+ return (
+
+ {!hideLabel && label && (
+
+ )}
+ {!hideLabel && !!description && (
+
+ )}
+
+
+ )
+}
+
+const MyCheckboxesWidget = (props: WidgetProps) => {
+ const {
+ id,
+ disabled,
+ options,
+ value,
+ readonly,
+ onChange,
+ onBlur,
+ onFocus,
+ autofocus = false,
+ } = props
+ const { enumOptions, enumDisabled, emptyValue } = options
+ const checkboxesValues = Array.isArray(value) ? value : [value]
+
+ return (
+
+ {Array.isArray(enumOptions) &&
+ enumOptions.map((option, index) => {
+ const checked = enumOptionsIsSelected(option.value, checkboxesValues)
+ const itemDisabled =
+ Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1
+ const disabledCls = disabled || itemDisabled || readonly ? "disabled" : ""
+
+ return (
+
+ )
+ })}
+
+ )
+}
+
// create Registry information
// templates
const myTemplates: Partial = {
@@ -217,6 +343,8 @@ const myTemplates: Partial = {
const myWidgets: RegistryWidgetsType = {
TextWidget: MyTextWidget,
EmailWidget: MyEmailWidget,
+ CheckboxWidget: MyCheckboxWidget,
+ CheckboxesWidget: MyCheckboxesWidget,
}
// create the overall theme to use on the other page
diff --git a/src/core/components/Filter.tsx b/src/core/components/Filter.tsx
index 8463e25c..9425fc7a 100644
--- a/src/core/components/Filter.tsx
+++ b/src/core/components/Filter.tsx
@@ -154,6 +154,8 @@ function DebouncedInput({
debounce?: number
} & Omit, "onChange">) {
const [value, setValue] = React.useState(initialValue)
+ const onChangeRef = React.useRef(onChange)
+ onChangeRef.current = onChange
React.useEffect(() => {
setValue(initialValue)
@@ -161,11 +163,11 @@ function DebouncedInput({
React.useEffect(() => {
const timeout = setTimeout(() => {
- onChange(value)
+ onChangeRef.current(value)
}, debounce)
return () => clearTimeout(timeout)
- }, [value, debounce, onChange])
+ }, [value, debounce])
return setValue(e.target.value)} />
}
diff --git a/src/core/components/Table.tsx b/src/core/components/Table.tsx
index 6ee0d6f5..d5fa2be6 100644
--- a/src/core/components/Table.tsx
+++ b/src/core/components/Table.tsx
@@ -1,5 +1,6 @@
import {
ColumnDef,
+ ColumnFiltersState,
FilterFn,
flexRender,
getCoreRowModel,
@@ -138,6 +139,8 @@ type TableProps = {
onPaginationChange?: OnChangeFn
pageCount?: number
pageSizeOptions?: number[]
+ onGlobalFilterChange?: (filter: string) => void
+ onColumnFiltersChange?: (filters: ColumnFiltersState) => void
classNames?: {
table?: string
thead?: string
@@ -191,9 +194,12 @@ const Table = ({
onPaginationChange,
pageCount: controlledPageCount,
pageSizeOptions = [5, 10, 20, 30, 40, 50],
+ onGlobalFilterChange,
+ onColumnFiltersChange,
}: TableProps) => {
const [sorting, setSorting] = React.useState([])
const [globalFilter, setGlobalFilter] = React.useState("")
+ const [columnFilters, setColumnFilters] = React.useState([])
const [internalPagination, setInternalPagination] = React.useState({
pageIndex: 0,
pageSize: 5,
@@ -222,10 +228,12 @@ const Table = ({
pageCount: manualPagination ? controlledPageCount : undefined,
state: {
sorting: sorting,
+ columnFilters: columnFilters,
globalFilter: globalFilter,
pagination: resolvedPaginationState,
},
onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: handlePaginationChange,
globalFilterFn: defaultGlobalFilterFn,
@@ -239,14 +247,41 @@ const Table = ({
const globalSearchTooltipId = React.useId()
React.useEffect(() => {
- if (!addPagination) {
+ if (!addPagination) return
+ if (!manualPagination && pageCount > 0 && pageIndex >= pageCount) {
+ table.setPageIndex(0)
+ }
+ }, [addPagination, pageCount, pageIndex, table, manualPagination])
+
+ const isFirstFilterRender = React.useRef(true)
+ React.useEffect(() => {
+ if (isFirstFilterRender.current) {
+ isFirstFilterRender.current = false
return
}
+ if (!addPagination) return
+ if (manualPagination) {
+ onGlobalFilterChange?.(globalFilter)
+ } else {
+ table.setPageIndex(0)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [globalFilter])
- if (!manualPagination && pageCount > 0 && pageIndex >= pageCount) {
+ const isFirstColumnFilterRender = React.useRef(true)
+ React.useEffect(() => {
+ if (isFirstColumnFilterRender.current) {
+ isFirstColumnFilterRender.current = false
+ return
+ }
+ if (!addPagination) return
+ if (manualPagination) {
+ onColumnFiltersChange?.(columnFilters)
+ } else {
table.setPageIndex(0)
}
- }, [addPagination, pageCount, pageIndex, table, manualPagination])
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [columnFilters])
return (
<>
diff --git a/src/core/styles/index.css b/src/core/styles/index.css
index 7e33b8de..b40c230b 100644
--- a/src/core/styles/index.css
+++ b/src/core/styles/index.css
@@ -52,13 +52,15 @@
width: 100%;
border: none;
margin: 0 auto;
- display: block;
- padding: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 16px;
border-radius: 12px;
background-color: oklch(var(--b3));
color: oklch(var(--bc));
font-size: 16px;
- text-align: center;
+ text-align: left;
}
/* form overall header */
@@ -69,11 +71,10 @@
font-family: inherit;
}
-.formHead-wrapper [class^="formHead"] div {
- width: 30%;
- display: inline-block;
- text-align: left;
- padding: 10px;
+.formHead-wrapper [class^="formHead"] > div {
+ width: 100%;
+ display: block;
+ padding: 0;
}
.move-icon {
@@ -117,6 +118,32 @@
height: 48px;
}
+/* markdown description wrapper — nested divs must not inherit the formHead > div reset */
+.formHead-wrapper .form-description-wrapper > div {
+ display: block;
+ width: 100%;
+ padding: 0;
+}
+
+.formHead-wrapper .form-description-wrapper .form-desc-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.25rem;
+}
+
+.formHead-wrapper .form-description-wrapper .join,
+.formHead-wrapper .form-description-wrapper .join > * {
+ display: inline-flex;
+ width: auto;
+}
+
+.formHead-wrapper textarea.form-description {
+ height: auto !important;
+ min-height: 8rem;
+ resize: vertical;
+}
+
.formHead-wrapper .card-modal-select {
background-color: oklch(var(--pc)) !important;
}
@@ -207,19 +234,20 @@
/* form visual checkboxes */
.rjsf .checkbox {
display: flex;
- align-items: center;
- width: 100%; /* Ensures the container takes up the full width */
+ flex-direction: column;
+ width: 100%;
}
/* form visual checkboxes */
.rjsf .checkbox label {
- flex: 1; /* Allows the label to expand to fit the content */
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
}
/* form visual checkboxes */
.rjsf .checkbox span {
- flex: 1; /* Allows the span to take up more space */
- white-space: normal; /* Allows text to wrap if needed */
+ white-space: normal;
}
/* form visual focus block */
@@ -674,6 +702,23 @@ span.arrow.tooltip-arrow {
margin-left: 30px;
}
+.rjsf .checkboxes-group {
+ width: 100%;
+}
+
+.rjsf .checkboxes-option {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 4px 0;
+ cursor: pointer;
+}
+
+.rjsf .checkboxes-option.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
.tooltip {
z-index: 9999; /* A higher value than the modal */
}
diff --git a/src/formBuilder/CardGeneralParameterInputs.tsx b/src/formBuilder/CardGeneralParameterInputs.tsx
index 2615242f..6e9d7efc 100644
--- a/src/formBuilder/CardGeneralParameterInputs.tsx
+++ b/src/formBuilder/CardGeneralParameterInputs.tsx
@@ -2,6 +2,7 @@ import React, { ReactElement } from "react"
import { Input, FormGroup, FormFeedback } from "reactstrap"
import classnames from "classnames"
import GeneralParameterInputs from "./GeneralParameterInputs"
+import MarkdownDescriptionInput from "./MarkdownDescriptionInput"
import SelectField from "src/core/components/fields/SelectField"
import {
defaultUiProps,
@@ -31,7 +32,6 @@ export default function CardGeneralParameterInputs({
const [keyState, setKeyState] = React.useState(parameters.name)
const [keyError, setKeyError] = React.useState(null)
const [titleState, setTitleState] = React.useState(parameters.title)
- const [descriptionState, setDescriptionState] = React.useState(parameters.description)
const [elementId] = React.useState(getRandomId())
const categoryMap = categoryToNameMap(allFormInputs)
@@ -59,6 +59,7 @@ export default function CardGeneralParameterInputs({
"date",
"time",
"checkbox",
+ "checkboxes",
"radio",
"dropdown",
"shortAnswer",
@@ -168,18 +169,10 @@ export default function CardGeneralParameterInputs({
type="help"
/>
-
- setDescriptionState(ev.target.value)}
- onBlur={(ev) => {
- onChange({ ...parameters, description: ev.target.value })
- }}
- className="card-text"
- />
-
+ onChange({ ...parameters, description: val })}
+ />
-
+ onChange={(val) =>
onChange(
stringify({
...schemaData,
- description: ev.target.value,
+ description: val,
}),
uischema
)
}
- className="form-description"
/>
diff --git a/src/formBuilder/MarkdownDescriptionInput.tsx b/src/formBuilder/MarkdownDescriptionInput.tsx
new file mode 100644
index 00000000..30180243
--- /dev/null
+++ b/src/formBuilder/MarkdownDescriptionInput.tsx
@@ -0,0 +1,53 @@
+import { useState } from "react"
+import ReactMarkdown from "react-markdown"
+import remarkGfm from "remark-gfm"
+import remarkBreaks from "remark-breaks"
+
+export default function MarkdownDescriptionInput({
+ value,
+ onChange,
+}: {
+ value: string
+ onChange: (v: string) => void
+}) {
+ const [mode, setMode] = useState<"edit" | "preview">("edit")
+
+ return (
+
+
+
+
+
+
+
Supports Markdown
+
+ {mode === "edit" ? (
+
+ )
+}
diff --git a/src/formBuilder/Section.tsx b/src/formBuilder/Section.tsx
index f485893b..7f9204da 100644
--- a/src/formBuilder/Section.tsx
+++ b/src/formBuilder/Section.tsx
@@ -2,6 +2,7 @@ import React, { ReactElement } from "react"
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"
import SelectField from "src/core/components/fields/SelectField"
import { Alert, Input, UncontrolledTooltip, FormGroup, FormFeedback } from "reactstrap"
+import MarkdownDescriptionInput from "./MarkdownDescriptionInput"
import FBCheckbox from "./checkbox/FBCheckbox"
import Collapse from "./Collapse/Collapse"
import CardModal from "./CardModal"
@@ -223,20 +224,9 @@ export default function Section({
type="help"
/>
-
- onChange(
- {
- ...schema,
- description: ev.target.value,
- },
- uischema
- )
- }
- className="card-text"
+ onChange={(val) => onChange({ ...schema, description: val }, uischema)}
/>
void
+}) {
+ const items = (parameters.items as any) || {}
+ const enumArray = Array.isArray(items.enum) ? items.enum : []
+ const [elementId] = React.useState(getRandomId())
+
+ return (
+
+
Options
+ {
+ const hasNames = Array.isArray(items.enumNames)
+ onChange({
+ ...parameters,
+ items: {
+ ...items,
+ enumNames: hasNames ? null : enumArray.map((val: any) => `${val}`),
+ },
+ })
+ }}
+ isChecked={Array.isArray(items.enumNames)}
+ label="Display different text label than the stored value"
+ id={`${elementId}_different`}
+ />
+ `${val}`) : undefined
+ }
+ showNames={Array.isArray(items.enumNames)}
+ onChange={(newEnum, newEnumNames) =>
+ onChange({
+ ...parameters,
+ items: { ...items, enum: newEnum, enumNames: newEnumNames },
+ })
+ }
+ type="string"
+ />
+
+ )
+}
+
const defaultInputs: { [key: string]: FormInput } = {
dateTime: {
displayName: "Date-Time",
@@ -195,7 +242,7 @@ const defaultInputs: { [key: string]: FormInput } = {
modalBody: CardDefaultParameterInputs,
},
checkbox: {
- displayName: "Checkbox",
+ displayName: "Yes / No",
matchIf: [
{
types: ["boolean"],
@@ -207,8 +254,27 @@ const defaultInputs: { [key: string]: FormInput } = {
cardBody: Checkbox,
modalBody: CardDefaultParameterInputs,
},
+ checkboxes: {
+ displayName: "Checkboxes (Multi-select)",
+ matchIf: [
+ {
+ types: ["array"],
+ widget: "checkboxes",
+ },
+ ],
+ defaultDataSchema: {
+ items: { type: "string", enum: [] },
+ uniqueItems: true,
+ },
+ defaultUiSchema: {
+ "ui:widget": "checkboxes",
+ },
+ type: "array",
+ cardBody: MultipleChoiceArray,
+ modalBody: CardDefaultParameterInputs,
+ },
radio: {
- displayName: "Radio",
+ displayName: "Radio (Single-select)",
matchIf: [
{
types: ["string", "number", "integer", "array", "boolean", "null"],
diff --git a/src/forms/components/FormsList.tsx b/src/forms/components/FormsList.tsx
index 76ff6cce..5b9cf44e 100644
--- a/src/forms/components/FormsList.tsx
+++ b/src/forms/components/FormsList.tsx
@@ -11,6 +11,7 @@ type FormsListProps = {
onPaginationChange?: OnChangeFn
pageCount?: number
pageSizeOptions?: number[]
+ onGlobalFilterChange?: (filter: string) => void
}
export const FormsList = ({
@@ -20,6 +21,7 @@ export const FormsList = ({
onPaginationChange,
pageCount,
pageSizeOptions,
+ onGlobalFilterChange,
}: FormsListProps) => {
const formsTableData = processForms(forms)
@@ -34,6 +36,7 @@ export const FormsList = ({
onPaginationChange={onPaginationChange}
pageCount={pageCount}
pageSizeOptions={pageSizeOptions}
+ onGlobalFilterChange={onGlobalFilterChange}
/>
)
diff --git a/src/invites/mutations/acceptInvite.ts b/src/invites/mutations/acceptInvite.ts
index bd1f9744..02ad9064 100644
--- a/src/invites/mutations/acceptInvite.ts
+++ b/src/invites/mutations/acceptInvite.ts
@@ -226,12 +226,9 @@ export default resolver.pipe(
ctx
)
- // Delete invitation(s) for that email and project Id
- await db.invitation.deleteMany({
- where: {
- email: invite.email,
- projectId: invite.projectId,
- },
+ // Delete this specific invitation by id so it can only be used once
+ await db.invitation.delete({
+ where: { id: invite.id },
})
return project
diff --git a/src/pages/forms/index.tsx b/src/pages/forms/index.tsx
index 23ff63d9..e69c5cbd 100644
--- a/src/pages/forms/index.tsx
+++ b/src/pages/forms/index.tsx
@@ -32,6 +32,7 @@ const AllFormsPage = () => {
pageIndex: 0,
pageSize: 10,
})
+ const [search, setSearch] = useState("")
const [selectedFolderId, setSelectedFolderId] = useState("all")
const [editingFolderId, setEditingFolderId] = useState(null)
const [editingFolderName, setEditingFolderName] = useState("")
@@ -57,6 +58,7 @@ const AllFormsPage = () => {
where: {
user: { id: currentUser?.id },
archived: false,
+ ...(search ? { name: { contains: search, mode: "insensitive" } } : {}),
...folderFilter,
},
orderBy: { id: "desc" },
@@ -71,6 +73,9 @@ const AllFormsPage = () => {
setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater))
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
const handleFolderSelect = (id: number | null | "all") => {
setSelectedFolderId(id)
setPagination((p) => ({ ...p, pageIndex: 0 }))
@@ -130,6 +135,17 @@ const AllFormsPage = () => {
onFormsUpdated={refetch}
/>
+
+
+
{/* Folder sidebar */}
diff --git a/src/pages/notifications/index.tsx b/src/pages/notifications/index.tsx
index af240f53..e1e2b0cb 100644
--- a/src/pages/notifications/index.tsx
+++ b/src/pages/notifications/index.tsx
@@ -15,7 +15,7 @@ import { MultiReadToggleButton } from "src/notifications/components/MultiReadTog
import { InformationCircleIcon } from "@heroicons/react/24/outline"
import { Tooltip } from "react-tooltip"
import Card from "src/core/components/Card"
-import { PaginationState } from "@tanstack/react-table"
+import { ColumnFiltersState, PaginationState } from "@tanstack/react-table"
import { Prisma } from "db"
const NotificationContent = () => {
@@ -31,23 +31,18 @@ const NotificationContent = () => {
pageIndex: 0,
pageSize: 10,
})
+ const [search, setSearch] = useState("")
+ const [columnFilters, setColumnFilters] = useState
([])
- const baseWhere = useMemo(
+ const coreWhere = useMemo(
() => ({
- recipients: {
- some: {
- id: currentUser!.id,
- },
- },
- // Only include notifications for projects where the contributor is not deleted
+ recipients: { some: { id: currentUser!.id } },
project: {
projectMembers: {
some: {
- users: {
- some: { id: currentUser!.id },
- },
- name: null, // Contributor (indicating it's not a team)
- deleted: false, // Only include undeleted project members
+ users: { some: { id: currentUser!.id } },
+ name: null,
+ deleted: false,
},
},
},
@@ -55,6 +50,28 @@ const NotificationContent = () => {
[currentUser]
)
+ const baseWhere = useMemo(() => {
+ const conditions: Prisma.NotificationWhereInput[] = [coreWhere]
+ if (search) {
+ conditions.push({
+ OR: [
+ { message: { contains: search, mode: "insensitive" } },
+ { project: { name: { contains: search, mode: "insensitive" } } },
+ ],
+ })
+ }
+ for (const filter of columnFilters) {
+ const value = String(filter.value ?? "").trim()
+ if (!value) continue
+ if (filter.id === "readStatus") {
+ conditions.push({ read: value === "Read" })
+ } else if (filter.id === "projectName") {
+ conditions.push({ project: { name: { contains: value, mode: "insensitive" } } })
+ }
+ }
+ return conditions.length === 1 ? conditions[0]! : { AND: conditions }
+ }, [coreWhere, search, columnFilters])
+
const paginationArgs = useMemo(
() => ({
skip: pagination.pageIndex * pagination.pageSize,
@@ -103,6 +120,18 @@ const NotificationContent = () => {
disableGlobalSelection()
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ disableGlobalSelection()
+ }
+
+ const handleColumnFiltersChange = (filters: ColumnFiltersState) => {
+ setColumnFilters(filters)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ disableGlobalSelection()
+ }
+
const handleActionCompleted = async () => {
await refetch()
resetSelection()
@@ -169,6 +198,8 @@ const NotificationContent = () => {
onPaginationChange={handlePaginationChange}
pageCount={pageCount}
pageSizeOptions={[10, 25, 50, 100]}
+ onGlobalFilterChange={handleGlobalFilterChange}
+ onColumnFiltersChange={handleColumnFiltersChange}
/>
diff --git a/src/pages/projects/[projectId]/notifications/index.tsx b/src/pages/projects/[projectId]/notifications/index.tsx
index f1a22586..76c0c46f 100644
--- a/src/pages/projects/[projectId]/notifications/index.tsx
+++ b/src/pages/projects/[projectId]/notifications/index.tsx
@@ -14,7 +14,7 @@ import { MultiReadToggleButton } from "src/notifications/components/MultiReadTog
import Card from "src/core/components/Card"
import { InformationCircleIcon } from "@heroicons/react/24/outline"
import { Tooltip } from "react-tooltip"
-import { PaginationState } from "@tanstack/react-table"
+import { ColumnFiltersState, PaginationState } from "@tanstack/react-table"
import { Prisma } from "db"
const NotificationContent = () => {
@@ -31,19 +31,37 @@ const NotificationContent = () => {
pageIndex: 0,
pageSize: 10,
})
+ const [search, setSearch] = useState("")
+ const [columnFilters, setColumnFilters] = useState([])
- const baseWhere = useMemo(
+ const coreWhere = useMemo(
() => ({
- recipients: {
- some: {
- id: currentUser!.id,
- },
- },
+ recipients: { some: { id: currentUser!.id } },
projectId: projectId ?? undefined,
}),
[currentUser, projectId]
)
+ const baseWhere = useMemo(() => {
+ const conditions: Prisma.NotificationWhereInput[] = [coreWhere]
+ if (search) {
+ conditions.push({
+ OR: [
+ { message: { contains: search, mode: "insensitive" } },
+ { project: { name: { contains: search, mode: "insensitive" } } },
+ ],
+ })
+ }
+ for (const filter of columnFilters) {
+ const value = String(filter.value ?? "").trim()
+ if (!value) continue
+ if (filter.id === "readStatus") {
+ conditions.push({ read: value === "Read" })
+ }
+ }
+ return conditions.length === 1 ? conditions[0]! : { AND: conditions }
+ }, [coreWhere, search, columnFilters])
+
const paginationArgs = useMemo(
() => ({
skip: pagination.pageIndex * pagination.pageSize,
@@ -93,6 +111,18 @@ const NotificationContent = () => {
disableGlobalSelection()
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ disableGlobalSelection()
+ }
+
+ const handleColumnFiltersChange = (filters: ColumnFiltersState) => {
+ setColumnFilters(filters)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ disableGlobalSelection()
+ }
+
const handleActionCompleted = async () => {
await refetch()
resetSelection()
@@ -158,6 +188,8 @@ const NotificationContent = () => {
onPaginationChange={handlePaginationChange}
pageCount={pageCount}
pageSizeOptions={[10, 25, 50, 100]}
+ onGlobalFilterChange={handleGlobalFilterChange}
+ onColumnFiltersChange={handleColumnFiltersChange}
/>
diff --git a/src/pages/projects/[projectId]/tasks/new.tsx b/src/pages/projects/[projectId]/tasks/new.tsx
index bc3ecc2b..d9bcf401 100644
--- a/src/pages/projects/[projectId]/tasks/new.tsx
+++ b/src/pages/projects/[projectId]/tasks/new.tsx
@@ -15,6 +15,135 @@ import Link from "next/link"
import { InformationCircleIcon } from "@heroicons/react/24/outline"
import { Tooltip } from "react-tooltip"
+const mapTaskToInitialValues = (task: any) => {
+ if (!task) return undefined
+
+ const initial: any = {}
+
+ // Only copy simple text/date/ids
+ if (typeof task.name === "string" && task.name.trim()) initial.name = `${task.name} (Copy)`
+ if ("description" in task) initial.description = task.description ?? null
+ if ("milestoneId" in task) initial.milestoneId = task.milestoneId ?? null
+ if ("deadline" in task && task.deadline) initial.deadline = new Date(task.deadline)
+ if ("formVersionId" in task) initial.formVersionId = task.formVersionId ?? null
+ if ("startDate" in task && task.startDate) initial.startDate = new Date(task.startDate)
+ if ("containerId" in task) initial.containerId = task.containerId ?? null
+ if ("anonymous" in task) initial.anonymous = task.anonymous ?? false
+
+ // Map contributors/teams from assignedMembers if present (kept minimal)
+ if (Array.isArray((task as any).assignedMembers)) {
+ const assigned = (task as any).assignedMembers as any[]
+ const contributors: number[] = []
+ const teams: number[] = []
+ for (const m of assigned) {
+ const id = typeof m?.id === "number" ? m.id : undefined
+ const usersLen = Array.isArray(m?.users) ? m.users.length : 0
+ if (typeof id === "number") {
+ if (usersLen > 1) teams.push(id)
+ else contributors.push(id)
+ }
+ }
+ if (contributors.length) initial.projectMembersId = contributors
+ if (teams.length) initial.teamsId = teams
+ }
+
+ // Map roles if included
+ if (Array.isArray((task as any).roles)) {
+ const rids = (task as any).roles
+ .map((r: any) => (typeof r?.id === "number" ? r.id : undefined))
+ .filter((n: any) => typeof n === "number")
+ if (rids.length) initial.rolesId = rids
+ }
+
+ // Copy simple numeric ID arrays (no relation includes)
+ const toNumArray = (arr: any) =>
+ Array.isArray(arr)
+ ? arr
+ .map((v) => (v == null ? v : Number(v)))
+ .filter((v) => typeof v === "number" && !Number.isNaN(v))
+ : undefined
+
+ const roleIds = toNumArray((task as any).rolesId)
+ if (roleIds && roleIds.length) initial.rolesId = roleIds
+
+ const memberIds = toNumArray((task as any).projectMembersId)
+ if (memberIds && memberIds.length) initial.projectMembersId = memberIds
+
+ const teamIds = toNumArray((task as any).teamsId)
+ if (teamIds && teamIds.length) initial.teamsId = teamIds
+
+ // Copy tags in UI shape expected by the tag input ({ id, text })
+ if (Array.isArray((task as any).tags)) {
+ const src = (task as any).tags as any[]
+ const uiTags = src
+ .map((t) => {
+ if (!t) return undefined
+ if (typeof t === "object") {
+ if ("id" in t && "text" in t && typeof t.id === "string" && typeof t.text === "string") {
+ return { id: t.id, text: t.text }
+ }
+ if (
+ "key" in t &&
+ "value" in t &&
+ typeof (t as any).key === "string" &&
+ typeof (t as any).value === "string"
+ ) {
+ return { id: (t as any).key, text: (t as any).value }
+ }
+ }
+ if (typeof t === "string") return { id: t, text: t }
+ return undefined
+ })
+ .filter(Boolean)
+ if (uiTags.length) initial.tags = uiTags as { id: string; text: string }[]
+ }
+
+ return initial
+}
+
+const TaskFormWrapper = ({ projectId, onSubmit, onCancel, schema }: any) => {
+ const router = useRouter()
+ const copyFromTaskIdParam = router.query.copyFromTaskId
+ const copyFromTaskId =
+ typeof copyFromTaskIdParam === "string"
+ ? parseInt(copyFromTaskIdParam, 10)
+ : Array.isArray(copyFromTaskIdParam)
+ ? parseInt(copyFromTaskIdParam[0]!, 10)
+ : undefined
+
+ const [sourceTask] = useQuery(
+ getTask,
+ {
+ where: { id: copyFromTaskId ?? -1 },
+ include: {
+ assignedMembers: { include: { users: { select: { id: true } } } },
+ roles: { select: { id: true } },
+ },
+ },
+ {
+ enabled: Boolean(copyFromTaskId),
+ suspense: false,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ }
+ )
+
+ const initialValues = mapTaskToInitialValues(sourceTask)
+
+ return (
+
+ )
+}
+
const NewTaskPage = () => {
const router = useRouter()
const [createTaskMutation] = useMutation(createTask)
@@ -61,141 +190,6 @@ const NewTaskPage = () => {
}
}
- const mapTaskToInitialValues = (task: any) => {
- if (!task) return undefined
-
- const initial: any = {}
-
- // Only copy simple text/date/ids
- if (typeof task.name === "string" && task.name.trim()) initial.name = `${task.name} (Copy)`
- if ("description" in task) initial.description = task.description ?? null
- if ("milestoneId" in task) initial.milestoneId = task.milestoneId ?? null
- if ("deadline" in task && task.deadline) initial.deadline = new Date(task.deadline)
- if ("formVersionId" in task) initial.formVersionId = task.formVersionId ?? null
- if ("startDate" in task && task.startDate) initial.startDate = new Date(task.startDate)
- if ("containerId" in task) initial.containerId = task.containerId ?? null
- if ("anonymous" in task) initial.anonymous = task.anonymous ?? false
-
- // Map contributors/teams from assignedMembers if present (kept minimal)
- if (Array.isArray((task as any).assignedMembers)) {
- const assigned = (task as any).assignedMembers as any[]
- const contributors: number[] = []
- const teams: number[] = []
- for (const m of assigned) {
- const id = typeof m?.id === "number" ? m.id : undefined
- const usersLen = Array.isArray(m?.users) ? m.users.length : 0
- if (typeof id === "number") {
- if (usersLen > 1) teams.push(id)
- else contributors.push(id)
- }
- }
- if (contributors.length) initial.projectMembersId = contributors
- if (teams.length) initial.teamsId = teams
- }
-
- // Map roles if included
- if (Array.isArray((task as any).roles)) {
- const rids = (task as any).roles
- .map((r: any) => (typeof r?.id === "number" ? r.id : undefined))
- .filter((n: any) => typeof n === "number")
- if (rids.length) initial.rolesId = rids
- }
-
- // Copy simple numeric ID arrays (no relation includes)
- const toNumArray = (arr: any) =>
- Array.isArray(arr)
- ? arr
- .map((v) => (v == null ? v : Number(v)))
- .filter((v) => typeof v === "number" && !Number.isNaN(v))
- : undefined
-
- const roleIds = toNumArray((task as any).rolesId)
- if (roleIds && roleIds.length) initial.rolesId = roleIds
-
- const memberIds = toNumArray((task as any).projectMembersId)
- if (memberIds && memberIds.length) initial.projectMembersId = memberIds
-
- const teamIds = toNumArray((task as any).teamsId)
- if (teamIds && teamIds.length) initial.teamsId = teamIds
-
- // Copy tags in UI shape expected by the tag input ({ id, text })
- if (Array.isArray((task as any).tags)) {
- const src = (task as any).tags as any[]
- const uiTags = src
- .map((t) => {
- if (!t) return undefined
- if (typeof t === "object") {
- if (
- "id" in t &&
- "text" in t &&
- typeof t.id === "string" &&
- typeof t.text === "string"
- ) {
- return { id: t.id, text: t.text }
- }
- if (
- "key" in t &&
- "value" in t &&
- typeof (t as any).key === "string" &&
- typeof (t as any).value === "string"
- ) {
- return { id: (t as any).key, text: (t as any).value }
- }
- }
- if (typeof t === "string") return { id: t, text: t }
- return undefined
- })
- .filter(Boolean)
- if (uiTags.length) initial.tags = uiTags as { id: string; text: string }[]
- }
-
- return initial
- }
-
- // Wrapper to handle copyFromTaskId and fetch the source task if needed
- const TaskFormWrapper = ({ projectId, onSubmit, onCancel, schema }: any) => {
- const router = useRouter()
- const copyFromTaskIdParam = router.query.copyFromTaskId
- const copyFromTaskId =
- typeof copyFromTaskIdParam === "string"
- ? parseInt(copyFromTaskIdParam, 10)
- : Array.isArray(copyFromTaskIdParam)
- ? parseInt(copyFromTaskIdParam[0]!, 10)
- : undefined
-
- const [sourceTask] = useQuery(
- getTask,
- {
- where: { id: copyFromTaskId ?? -1 },
- include: {
- assignedMembers: { include: { users: { select: { id: true } } } },
- roles: { select: { id: true } },
- },
- },
- {
- enabled: Boolean(copyFromTaskId),
- suspense: false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- }
- )
-
- const initialValues = mapTaskToInitialValues(sourceTask)
-
- return (
-
- )
- }
-
return (
// @ts-expect-error children are clearly passed below
diff --git a/src/pages/projects/[projectId]/teams/index.tsx b/src/pages/projects/[projectId]/teams/index.tsx
index 9139ac00..2f4f9d9e 100644
--- a/src/pages/projects/[projectId]/teams/index.tsx
+++ b/src/pages/projects/[projectId]/teams/index.tsx
@@ -29,11 +29,12 @@ export const AllTeamList = ({ privilege, projectId }: AllTeamListProps) => {
pageIndex: 0,
pageSize: 10,
})
+ const [search, setSearch] = useState("")
const [{ projectMembers, count }] = usePaginatedQuery(getProjectMembers, {
where: {
projectId: projectId,
- name: { not: null }, // Ensures the name in ProjectMember is non-null
+ name: { not: null, ...(search ? { contains: search, mode: "insensitive" } : {}) },
users: {
some: { id: { not: undefined } }, // Ensures there's at least one user
},
@@ -67,6 +68,11 @@ export const AllTeamList = ({ privilege, projectId }: AllTeamListProps) => {
setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater))
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
return (
{
onPaginationChange={handlePaginationChange}
pageCount={pageCount}
pageSizeOptions={[10, 25, 50, 100]}
+ onGlobalFilterChange={handleGlobalFilterChange}
/>
)
diff --git a/src/pages/roles/index.tsx b/src/pages/roles/index.tsx
index 40102354..cce02c64 100644
--- a/src/pages/roles/index.tsx
+++ b/src/pages/roles/index.tsx
@@ -17,6 +17,7 @@ const RoleBuilderPage = () => {
pageIndex: 0,
pageSize: 10,
})
+ const [search, setSearch] = useState("")
const paginationArgs = useMemo(
() => ({
@@ -27,7 +28,17 @@ const RoleBuilderPage = () => {
)
const [{ roles, count }, { refetch: refetchPagedRoles }] = usePaginatedQuery(getRoles, {
- where: { user: { id: currentUser?.id } },
+ where: {
+ user: { id: currentUser?.id },
+ ...(search
+ ? {
+ OR: [
+ { name: { contains: search, mode: "insensitive" } },
+ { taxonomy: { contains: search, mode: "insensitive" } },
+ ],
+ }
+ : {}),
+ },
orderBy: { id: "asc" },
...paginationArgs,
})
@@ -59,6 +70,11 @@ const RoleBuilderPage = () => {
setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater))
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
return (
// @ts-expect-error children are clearly passed below
@@ -91,6 +107,7 @@ const RoleBuilderPage = () => {
onPaginationChange={handlePaginationChange}
pageCount={pageCount}
pageSizeOptions={[10, 25, 50, 100]}
+ onGlobalFilterChange={handleGlobalFilterChange}
/>
diff --git a/src/roles/components/AllRolesList.tsx b/src/roles/components/AllRolesList.tsx
index 91461668..79471190 100644
--- a/src/roles/components/AllRolesList.tsx
+++ b/src/roles/components/AllRolesList.tsx
@@ -13,6 +13,7 @@ interface AllRolesListProps {
onPaginationChange?: OnChangeFn
pageCount?: number
pageSizeOptions?: number[]
+ onGlobalFilterChange?: (filter: string) => void
}
export const AllRolesList = ({
@@ -24,6 +25,7 @@ export const AllRolesList = ({
onPaginationChange,
pageCount,
pageSizeOptions,
+ onGlobalFilterChange,
}: AllRolesListProps) => {
// Process table data
const roleTableData = processRoleTableData(roles, onRolesChanged, taxonomyList)
@@ -39,6 +41,7 @@ export const AllRolesList = ({
onPaginationChange={onPaginationChange}
pageCount={pageCount}
pageSizeOptions={pageSizeOptions}
+ onGlobalFilterChange={onGlobalFilterChange}
/>
)
diff --git a/src/roles/components/ContributorsTab.tsx b/src/roles/components/ContributorsTab.tsx
index bb8ebbd2..abec5c24 100644
--- a/src/roles/components/ContributorsTab.tsx
+++ b/src/roles/components/ContributorsTab.tsx
@@ -8,11 +8,17 @@ import { AddRoleModal } from "./AddRoleModal"
import { ProjectMemberWithUsersAndRoles } from "src/core/types"
import Link from "next/link"
import { Tooltip } from "react-tooltip"
-import { PaginationState } from "@tanstack/react-table"
+import { ColumnFiltersState, PaginationState } from "@tanstack/react-table"
const ContributorsTab = () => {
const projectId = useParam("projectId", "number")
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
+ const [search, setSearch] = useState("")
+ const [columnFilters, setColumnFilters] = useState([])
+
+ const roleNameFilter = columnFilters.find((f) => f.id === "roleNames")?.value as
+ | string
+ | undefined
const [{ projectMembers: contributors, count }, { refetch }] = usePaginatedQuery(
getProjectMembers,
@@ -20,10 +26,22 @@ const ContributorsTab = () => {
where: {
projectId: projectId,
users: {
- every: {
- id: { not: undefined }, // Ensures there's at least one user
- },
+ every: { id: { not: undefined } }, // Ensures there's at least one user
+ ...(search
+ ? {
+ some: {
+ OR: [
+ { username: { contains: search, mode: "insensitive" } },
+ { firstName: { contains: search, mode: "insensitive" } },
+ { lastName: { contains: search, mode: "insensitive" } },
+ ],
+ },
+ }
+ : {}),
},
+ ...(roleNameFilter
+ ? { roles: { some: { name: { contains: roleNameFilter, mode: "insensitive" } } } }
+ : {}),
deleted: undefined,
name: { equals: null }, // Ensures ProjectMember is contributor and not team
},
@@ -42,6 +60,16 @@ const ContributorsTab = () => {
setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater))
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
+ const handleColumnFiltersChange = (filters: ColumnFiltersState) => {
+ setColumnFilters(filters)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
return (
@@ -54,6 +82,8 @@ const ContributorsTab = () => {
onPaginationChange={handlePaginationChange}
pageCount={pageCount}
pageSizeOptions={[10, 25, 50, 100]}
+ onGlobalFilterChange={handleGlobalFilterChange}
+ onColumnFiltersChange={handleColumnFiltersChange}
/>
pageCount?: number
pageSizeOptions?: number[]
+ onGlobalFilterChange?: (filter: string) => void
+ onColumnFiltersChange?: (filters: ColumnFiltersState) => void
}
export const RoleContributorTable = ({
@@ -18,6 +20,8 @@ export const RoleContributorTable = ({
onPaginationChange,
pageCount,
pageSizeOptions,
+ onGlobalFilterChange,
+ onColumnFiltersChange,
}: RoleContributorTableProps) => {
const processedData = contributors.map((contributor) => ({
username: contributor.users[0].username,
@@ -39,6 +43,8 @@ export const RoleContributorTable = ({
onPaginationChange={onPaginationChange}
pageCount={pageCount}
pageSizeOptions={pageSizeOptions}
+ onGlobalFilterChange={onGlobalFilterChange}
+ onColumnFiltersChange={onColumnFiltersChange}
/>
)
}
diff --git a/src/tasks/components/AllTaskList.tsx b/src/tasks/components/AllTaskList.tsx
index e3158fbe..9503fd83 100644
--- a/src/tasks/components/AllTaskList.tsx
+++ b/src/tasks/components/AllTaskList.tsx
@@ -8,7 +8,7 @@ import { AllTasksColumns } from "../tables/columns/AllTasksColumns"
import { TaskLogWithTaskProjectAndComments } from "src/core/types"
import Card from "src/core/components/Card"
import { useState } from "react"
-import { PaginationState } from "@tanstack/react-table"
+import { ColumnFiltersState, PaginationState } from "@tanstack/react-table"
type TaskWithLogs = TaskLogWithTaskProjectAndComments["task"] & {
taskLogs: TaskLogWithTaskProjectAndComments[]
@@ -20,6 +20,13 @@ export const AllTasksList = () => {
pageIndex: 0,
pageSize: 10,
})
+ const [search, setSearch] = useState("")
+ const [columnFilters, setColumnFilters] = useState
([])
+
+ const nameFilter = columnFilters.find((f) => f.id === "name")?.value as string | undefined
+ const projectNameFilter = columnFilters.find((f) => f.id === "projectName")?.value as
+ | string
+ | undefined
const [{ tasks, count }] = usePaginatedQuery(getTasks, {
where: {
@@ -32,6 +39,16 @@ export const AllTasksList = () => {
},
},
},
+ ...(search
+ ? {
+ OR: [
+ { name: { contains: search, mode: "insensitive" } },
+ { project: { name: { contains: search, mode: "insensitive" } } },
+ ],
+ }
+ : {}),
+ ...(nameFilter ? { name: { contains: nameFilter, mode: "insensitive" } } : {}),
+ ...(projectNameFilter ? { project: { name: projectNameFilter } } : {}),
},
include: {
project: true,
@@ -93,6 +110,16 @@ export const AllTasksList = () => {
setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater))
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
+ const handleColumnFiltersChange = (filters: ColumnFiltersState) => {
+ setColumnFilters(filters)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
return (
@@ -105,6 +132,8 @@ export const AllTasksList = () => {
onPaginationChange={handlePaginationChange}
pageCount={pageCount}
pageSizeOptions={[10, 25, 50, 100]}
+ onGlobalFilterChange={handleGlobalFilterChange}
+ onColumnFiltersChange={handleColumnFiltersChange}
/>
Note: This list only shows comment notifications for tasks that are explicitly assigned to
diff --git a/src/tasks/components/ProjectTasksList.tsx b/src/tasks/components/ProjectTasksList.tsx
index ea959fb6..b7859674 100644
--- a/src/tasks/components/ProjectTasksList.tsx
+++ b/src/tasks/components/ProjectTasksList.tsx
@@ -3,7 +3,7 @@ import { ProjectTasksColumns } from "src/tasks/tables/columns/ProjectTasksColumn
import Table from "src/core/components/Table"
import useProjecTasksListData from "../hooks/useProjectTasksListData"
import { useState } from "react"
-import { PaginationState } from "@tanstack/react-table"
+import { ColumnFiltersState, PaginationState } from "@tanstack/react-table"
export const ProjectTasksList = () => {
const projectId = useParam("projectId", "number")
@@ -11,8 +11,10 @@ export const ProjectTasksList = () => {
pageIndex: 0,
pageSize: 10,
})
+ const [search, setSearch] = useState("")
+ const [columnFilters, setColumnFilters] = useState([])
- const { tasks, count } = useProjecTasksListData(projectId, pagination)
+ const { tasks, count } = useProjecTasksListData(projectId, pagination, search, columnFilters)
const pageCount = Math.max(1, Math.ceil((count ?? 0) / pagination.pageSize))
const handlePaginationChange = (
@@ -21,6 +23,16 @@ export const ProjectTasksList = () => {
setPagination((prev) => (typeof updater === "function" ? updater(prev) : updater))
}
+ const handleGlobalFilterChange = (filter: string) => {
+ setSearch(filter)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
+ const handleColumnFiltersChange = (filters: ColumnFiltersState) => {
+ setColumnFilters(filters)
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }))
+ }
+
return (
@@ -33,6 +45,8 @@ export const ProjectTasksList = () => {
onPaginationChange={handlePaginationChange}
pageCount={pageCount}
pageSizeOptions={[10, 25, 50, 100]}
+ onGlobalFilterChange={handleGlobalFilterChange}
+ onColumnFiltersChange={handleColumnFiltersChange}
/>
diff --git a/src/tasks/hooks/useProjectTasksListData.ts b/src/tasks/hooks/useProjectTasksListData.ts
index 0bb616d5..72d61950 100644
--- a/src/tasks/hooks/useProjectTasksListData.ts
+++ b/src/tasks/hooks/useProjectTasksListData.ts
@@ -2,15 +2,17 @@ import { useEffect, useMemo, useState } from "react"
import { usePaginatedQuery, useQuery } from "@blitzjs/rpc"
import { useCurrentUser } from "src/users/hooks/useCurrentUser"
import getTasks, { GetTasksInput } from "../queries/getTasks"
-import { MemberPrivileges } from "@prisma/client"
+import { MemberPrivileges, Status } from "@prisma/client"
import { useMemberPrivileges } from "src/projectprivileges/components/MemberPrivilegesContext"
import { processProjectTasks } from "../tables/processing/processProjectTasks"
import getUserProjectMemberIds from "src/tasks/queries/getUserProjectMemberIds"
-import { PaginationState } from "@tanstack/react-table"
+import { ColumnFiltersState, PaginationState } from "@tanstack/react-table"
export default function useProjectTasksListData(
projectId: number | undefined,
- pagination: PaginationState
+ pagination: PaginationState,
+ search: string = "",
+ columnFilters: ColumnFiltersState = []
) {
const currentUser = useCurrentUser()
const { privilege } = useMemberPrivileges()
@@ -23,8 +25,22 @@ export default function useProjectTasksListData(
useEffect(() => {
if (!privilege || !currentUser || !projectId) return
+ const nameFilter = columnFilters.find((f) => f.id === "name")?.value as string | undefined
+ const containerFilter = columnFilters.find((f) => f.id === "container")?.value as
+ | string
+ | undefined
+ const statusFilter = columnFilters.find((f) => f.id === "status")?.value as string | undefined
+
let baseParams: GetTasksInput = {
- where: { project: { id: projectId } },
+ where: {
+ project: { id: projectId },
+ ...(search ? { name: { contains: search, mode: "insensitive" } } : {}),
+ ...(nameFilter ? { name: { contains: nameFilter, mode: "insensitive" } } : {}),
+ ...(containerFilter ? { container: { name: containerFilter } } : {}),
+ ...(statusFilter
+ ? { status: statusFilter === "Completed" ? Status.COMPLETED : Status.NOT_COMPLETED }
+ : {}),
+ },
orderBy: [{ id: "asc" }],
include: {
container: {
@@ -104,7 +120,7 @@ export default function useProjectTasksListData(
}
setQueryParams(baseParams)
- }, [privilege, currentUser, projectId, userMemberIds])
+ }, [privilege, currentUser, projectId, userMemberIds, search, columnFilters])
const queryInput = useMemo(() => {
const base = queryParams ?? {