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" ? ( +