diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b05443e..a5835c9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,94 +1,109 @@ module.exports = { - 'env': { - 'browser': true, - 'node': true, + env: { + browser: true, + node: true, }, - 'extends': [ + extends: [ 'plugin:react/recommended', 'plugin:import/typescript', - 'plugin:@tanstack/eslint-plugin-query/recommended', + 'plugin:storybook/recommended', 'plugin:react-hooks/recommended', ], - 'plugins': ['react', '@typescript-eslint', 'import'], - 'settings': { + plugins: ['react', '@typescript-eslint', 'import'], + settings: { + react: { + version: 'detect', + }, 'import/resolver': { 'typescript': {}, }, }, - 'parser': '@typescript-eslint/parser', - 'parserOptions': { - 'ecmaFeatures': { - 'jsx': true, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, }, - 'ecmaVersion': 'latest', - 'requireConfigFile': false, + ecmaVersion: 'latest', + sourceType: 'module', + requireConfigFile: false, }, - 'ignorePatterns': ['app/frontend/types/**/*', 'app/javascript/**/*'], - 'rules': { - 'indent': 'off', + ignorePatterns: ['app/javascript/**/*'], + rules: { + indent: 'off', '@typescript-eslint/indent': ['error', 'tab', { - 'SwitchCase': 1, - 'VariableDeclarator': 'first', - 'MemberExpression': 1, - 'ArrayExpression': 1, + SwitchCase: 1, + VariableDeclarator: 'first', + MemberExpression: 1, + ArrayExpression: 1, + ignoredNodes: ['TSTypeParameterInstantiation'], }], '@typescript-eslint/member-delimiter-style': ['error', { - 'multiline': { - 'delimiter': 'none', + multiline: { + delimiter: 'none', }, - 'singleline': { - 'delimiter': 'comma', + singleline: { + delimiter: 'comma', }, - 'multilineDetection': 'brackets', + multilineDetection: 'brackets', }], 'linebreak-style': ['error', 'unix'], - 'quotes': ['error', 'single'], - 'semi': ['error', 'never'], + quotes: ['error', 'single'], + semi: ['error', 'never'], 'no-unused-vars': ['warn', { - 'vars': 'all', - 'args': 'none', + vars: 'all', + args: 'none', }], 'no-prototype-builtins': [0], 'space-infix-ops': ['error'], 'no-trailing-spaces': 'error', 'object-curly-spacing': [2, 'always', { - 'objectsInObjects': true, + objectsInObjects: true, }], 'computed-property-spacing': 2, 'array-bracket-spacing': 0, 'brace-style': ['error', '1tbs', { - 'allowSingleLine': true, + allowSingleLine: true, }], 'react/boolean-prop-naming': ['error'], 'react/no-typos': ['error'], 'react/jsx-curly-spacing': ['error', { - 'when': 'always', - 'children': true, + when: 'always', + children: true, }], // 'react/jsx-space-before-closing': 2, 'react/jsx-tag-spacing': ['error', { - 'closingSlash': 'never', - 'beforeSelfClosing': 'always', - 'afterOpening': 'never', - 'beforeClosing': 'allow', + closingSlash: 'never', + beforeSelfClosing: 'always', + afterOpening: 'never', + beforeClosing: 'allow', }], 'react/display-name': ['off'], 'react/prop-types': 0, - 'eqeqeq': 'error', + eqeqeq: 'error', 'no-console': 'warn', 'eol-last': ['error', 'always'], '@typescript-eslint/keyword-spacing': [2, { - 'after': true, - 'before': true, - 'overrides': { - 'if': { 'after': false }, - 'for': { 'after': false }, - 'while': { 'after': false }, - 'switch': { 'after': false }, - 'catch': { 'after': false }, + after: true, + before: true, + overrides: { + if: { after: false }, + for: { after: false }, + while: { after: false }, + switch: { after: false }, + catch: { after: false }, }, }], 'comma-dangle': ['error', 'always-multiline'], 'react-hooks/exhaustive-deps': 0, }, + overrides: [ + { + files: ['*.d.ts'], + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/member-delimiter-style': 'off', + '@typescript-eslint/indent': 'off', + }, + }, + ], } diff --git a/app/controllers/api/controls_controller.rb b/app/controllers/api/controls_controller.rb index fb79649..e7148d2 100644 --- a/app/controllers/api/controls_controller.rb +++ b/app/controllers/api/controls_controller.rb @@ -27,8 +27,8 @@ def create end end - # @route PATCH /api/controls/:slug (api_control) - # @route PUT /api/controls/:slug (api_control) + # @route PATCH /api/controls/:id (api_control) + # @route PUT /api/controls/:id (api_control) def update if control.save render json: control.render, status: :created diff --git a/app/controllers/api/protocols_controller.rb b/app/controllers/api/protocols_controller.rb index c519bd7..7a11327 100644 --- a/app/controllers/api/protocols_controller.rb +++ b/app/controllers/api/protocols_controller.rb @@ -1,6 +1,12 @@ class Api::ProtocolsController < ApplicationController + expose :protocols, -> { Protocol.includes_associated.all } expose :protocol - expose :protocols, -> { Protocol.all } + + # @route GET /api/protocols/:slug (api_protocol) + def show + authorize protocol + render json: protocol.render(view: :show), status: :ok + end # @route GET /api/options/protocols (api_protocols_options) def options diff --git a/app/controllers/controls_controller.rb b/app/controllers/controls_controller.rb index 4c0ebc7..bd310eb 100644 --- a/app/controllers/controls_controller.rb +++ b/app/controllers/controls_controller.rb @@ -7,8 +7,9 @@ class ControlsController < ApplicationController # @route POST /controls (controls) def create authorize Control.new + ap({ control: }) if control.save - redirect_to control, notice: "Control was successfully created." + redirect_to edit_screen_path(control.screen), notice: "Control was successfully created." else redirect_to new_control_path, inertia: { errors: control.errors } end @@ -18,8 +19,9 @@ def create # @route PUT /controls/:id (control) def update authorize control + if control.update(control_params) - redirect_to control, notice: "Control was successfully updated." + redirect_to edit_screen_path(control.screen), inertia: { method: :get }, notice: "Control was successfully updated." else redirect_to edit_control_path, inertia: { errors: control.errors } end @@ -35,10 +37,10 @@ def destroy private def sortable_fields - %w(title type screen_id position min_value max_value value protocol_id).freeze + %w(title type screen_id min_value max_value value protocol_id).freeze end def control_params - params.require(:control).permit(:title, :type, :screen_id, :position, :min_value, :max_value, :value, :protocol_id) + params.require(:control).permit(:title, :control_type, :order, :color, :screen_id, :min_value, :max_value, :value, :protocol_id) end end diff --git a/app/controllers/protocols_controller.rb b/app/controllers/protocols_controller.rb index eadb37f..893fb0f 100644 --- a/app/controllers/protocols_controller.rb +++ b/app/controllers/protocols_controller.rb @@ -1,12 +1,13 @@ class ProtocolsController < ApplicationController include Searchable - expose :protocols, -> { search(Protocol.includes_associated, sortable_fields) } + expose :protocols, -> { search(Protocol, sortable_fields) } expose :protocol, id: -> { params[:slug] }, scope: -> { Protocol.includes_associated }, find_by: :slug # @route GET /protocols (protocols) def index authorize protocols + paginated_protocols = protocols.page(params[:page] || 1) render inertia: "Protocols/Index", props: { @@ -18,7 +19,7 @@ def index } end - # @route GET /protocols/:id (protocol) + # @route GET /protocols/:slug (protocol) def show authorize protocol render inertia: "Protocols/Show", props: { @@ -34,7 +35,7 @@ def new } end - # @route GET /protocols/:id/edit (edit_protocol) + # @route GET /protocols/:slug/edit (edit_protocol) def edit authorize protocol @@ -53,8 +54,8 @@ def create end end - # @route PATCH /protocols/:id (protocol) - # @route PUT /protocols/:id (protocol) + # @route PATCH /protocols/:slug (protocol) + # @route PUT /protocols/:slug (protocol) def update authorize protocol @@ -72,7 +73,7 @@ def update end end - # @route DELETE /protocols/:id (protocol) + # @route DELETE /protocols/:slug (protocol) def destroy authorize protocol protocol.destroy! diff --git a/app/controllers/screens_controller.rb b/app/controllers/screens_controller.rb index f177d81..cc6f3c9 100644 --- a/app/controllers/screens_controller.rb +++ b/app/controllers/screens_controller.rb @@ -31,7 +31,7 @@ def edit authorize screen render inertia: "Screens/Edit", props: { - screen: -> { screen.render(view: :edit) }, + screen: screen.render(view: :edit), screens: -> { Screen.all.render(view: :options) }, } end @@ -49,10 +49,12 @@ def create # @route PATCH /screens/:slug (screen) # @route PUT /screens/:slug (screen) def update + ap({ params:, screen_params: }) authorize screen if screen.update(screen_params) redirect_to screen, notice: "Screen was successfully updated." else + ap({ errors: screen.errors }) redirect_to edit_screen_path, inertia: { errors: screen.errors } end end @@ -71,6 +73,8 @@ def sortable_fields end def screen_params - params.require(:screen).permit(:title, :order) + params.require(:screen).permit(:title, :order, controls_attributes: [ + :id, :title, :order + ],) end end diff --git a/app/frontend/Components/Button/EditButton.tsx b/app/frontend/Components/Button/EditButton.tsx index 7eb0e3c..8f35ba5 100644 --- a/app/frontend/Components/Button/EditButton.tsx +++ b/app/frontend/Components/Button/EditButton.tsx @@ -1,9 +1,9 @@ import React from 'react' import { Link } from '@/Components' import { EditIcon } from '@/Components/Icons' -import { ILinkProps } from '../Link' +import { LinkProps } from '../Link' -interface IEditButtonProps extends Omit { +interface IEditButtonProps extends Omit { label?: string } diff --git a/app/frontend/Components/Button/ModalFormButton.tsx b/app/frontend/Components/Button/ModalFormButton.tsx deleted file mode 100644 index 5df0386..0000000 --- a/app/frontend/Components/Button/ModalFormButton.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useState } from 'react' -import Button from './index' -import { Modal } from '@/Components' -import { useMantineTheme, type ModalProps, type ButtonProps } from '@mantine/core' -import axios from 'axios' - -interface ModalFormButtonProps { - children?: string | React.ReactElement - form: React.ReactElement - title: string - buttonProps?: ButtonProps - modalProps?: Partial - onSubmit?: (form: Inertia.FormProps) => boolean|void - onSuccess?: (data: { id: string|number }) => void -} - -const ModalFormButton = ({ - children = 'New', - form, - title, - buttonProps = {}, - modalProps = {}, - onSubmit, - onSuccess, -}: ModalFormButtonProps) => { - const theme = useMantineTheme() - - const handleSubmit = ({ data, method, to, setError }: Inertia.FormProps) => { - if(!to) return - - axios[method](to, { ...data, redirect: false }) - .then(response => { - if(response.statusText === 'OK' || response.statusText === 'Created') { - if(onSuccess) onSuccess(response.data) - } - }) - .catch(error => { - if(error.response.data?.errors) { - setError(error.response.data.errors) - } - }) - - // Return false to prevent default form action - return false - } - - return ( - <> - { children } } - title={ title } - { ...Object.assign({ size: theme.breakpoints.md }, modalProps) } - > - { close => React.cloneElement(form, { - onSubmit: onSubmit ? onSubmit : handleSubmit, - }) } - - - ) -} - -export default ModalFormButton diff --git a/app/frontend/Components/Button/ToggleColorSchemeButton.tsx b/app/frontend/Components/Button/ToggleColorSchemeButton.tsx index e1cde0a..eb1acc0 100644 --- a/app/frontend/Components/Button/ToggleColorSchemeButton.tsx +++ b/app/frontend/Components/Button/ToggleColorSchemeButton.tsx @@ -1,8 +1,10 @@ import React from 'react' -import { ActionIcon, useComputedColorScheme, useMantineColorScheme } from '@mantine/core' +import { ActionIcon, type ActionIconProps, useComputedColorScheme, useMantineColorScheme } from '@mantine/core' import { SunIcon, MoonIcon } from '@/Components/Icons' -const ToggleColorSchemeButton = () => { +interface ToggleColorSchemeButtonProps extends ActionIconProps {} + +const ToggleColorSchemeButton = (props: ToggleColorSchemeButtonProps) => { const { colorScheme, setColorScheme } = useMantineColorScheme() const computedColorScheme = useComputedColorScheme('dark') @@ -17,6 +19,7 @@ const ToggleColorSchemeButton = () => { title="Toggle color scheme" style={ { display: 'inline-flex' } } aria-label={ `Toggle color scheme to ${colorScheme === 'dark' ? 'light' : 'dark'} mode` } + { ...props } > { colorScheme === 'dark' ? : } diff --git a/app/frontend/Components/Button/index.ts b/app/frontend/Components/Button/index.ts index bd6a12e..30db904 100644 --- a/app/frontend/Components/Button/index.ts +++ b/app/frontend/Components/Button/index.ts @@ -5,4 +5,3 @@ export { IconButton } export { default as EditButton } from './EditButton' export { default as DeleteButton } from './DeleteButton' export { default as ToggleColorSchemeButton } from './ToggleColorSchemeButton' -export { default as ModalFormButton } from './ModalFormButton' diff --git a/app/frontend/Components/ConditionalWrapper/index.tsx b/app/frontend/Components/ConditionalWrapper/index.tsx index 7d63f82..1af60e5 100644 --- a/app/frontend/Components/ConditionalWrapper/index.tsx +++ b/app/frontend/Components/ConditionalWrapper/index.tsx @@ -1,11 +1,20 @@ import React from 'react' -interface IConditionalWrapperProps { - children: JSX.Element +interface ConditionalWrapperProps { + children: JSX.Element|React.ReactNode condition: boolean - wrapper: (children: React.ReactNode) => JSX.Element + wrapper: (children: JSX.Element|React.ReactNode) => JSX.Element + elseWrapper?: (children: JSX.Element|React.ReactNode) => JSX.Element } -const ConditionalWrapper = ({ children, condition, wrapper }: IConditionalWrapperProps) => condition ? wrapper(children) : children +const ConditionalWrapper = ({ children, condition, wrapper, elseWrapper }: ConditionalWrapperProps) => { + if(condition) { + return wrapper(children) + } else if(elseWrapper) { + return elseWrapper(children) + } + + return <>{ children } +} export default ConditionalWrapper diff --git a/app/frontend/Components/Dropdowns/CommandDropdown.tsx b/app/frontend/Components/Dropdowns/CommandDropdown.tsx index 67b0caa..2fa5a9e 100644 --- a/app/frontend/Components/Dropdowns/CommandDropdown.tsx +++ b/app/frontend/Components/Dropdowns/CommandDropdown.tsx @@ -1,27 +1,29 @@ -import React, { forwardRef } from 'react' +import React from 'react' import { Select } from '@/Components/Form' import { type AsyncDropdown } from '.' import { useGetCommands } from '@/queries' -const CommandDropdown = forwardRef>(( - { label = 'Command', name = 'command_id', initialData = [], value, onSelect, ...props }, - ref, -) => { +const CommandDropdown = ({ + label = 'Command', + name = 'command_id', + initialData = [], + value, + onSelect, + ...props +}: AsyncDropdown) => { const { data } = useGetCommands() return ( ) -}) +} export default ProtocolDropdown diff --git a/app/frontend/Components/Dropdowns/CommandValueDropdown.tsx b/app/frontend/Components/Dropdowns/CommandValueDropdown.tsx index a8a2792..9d0dee4 100644 --- a/app/frontend/Components/Dropdowns/CommandValueDropdown.tsx +++ b/app/frontend/Components/Dropdowns/CommandValueDropdown.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef } from 'react' +import React from 'react' import { Select } from '@/Components/Form' import { type AsyncDropdown } from '.' import { useGetCommand } from '@/queries' @@ -7,15 +7,19 @@ interface CommandValueDropdownProps extends AsyncDropdown { commandSlug: string } -const CommandValueDropdown = forwardRef(( - { label = 'Command Value', name = 'command_value_id', commandSlug, initialData = [], value, onSelect, ...props }, - ref, -) => { - const { data } = useGetCommand(commandSlug) +const CommandValueDropdown = ({ + label = 'Command Value', + name = 'command_value_id', + commandSlug, + initialData = [], + value, + onSelect, + ...props +}: CommandValueDropdownProps) => { + const { data } = useGetCommand({ slug: commandSlug }) return ( ({ label: protocol.title, - value: protocol.slug, + value: String(protocol.id), + slug: protocol.slug, })) } { ...props } /> ) -}) +} export default ProtocolDropdown diff --git a/app/frontend/Components/Dropdowns/ServerDropdown.tsx b/app/frontend/Components/Dropdowns/ServerDropdown.tsx index e518ee9..a839eeb 100644 --- a/app/frontend/Components/Dropdowns/ServerDropdown.tsx +++ b/app/frontend/Components/Dropdowns/ServerDropdown.tsx @@ -1,17 +1,20 @@ -import React, { forwardRef } from 'react' +import React from 'react' import { Select } from '@/Components/Form' import { type AsyncDropdown } from '.' import { useGetServers } from '@/queries' -const ServerDropdown = forwardRef>(( - { label = 'Server', name = 'server_id', initialData = [], value, onSelect, ...props }, - ref, -) => { +const ServerDropdown = ({ + label = 'Server', + name = 'server_id', + initialData = [], + value, + onSelect, + ...props +}: AsyncDropdown) => { const { data } = useGetServers() return ( { + e.stopPropagation() + onClick?.(e) + } } { ...props } /> - + ) }) -export default React.memo(SelectComponent) +export default SelectComponent diff --git a/app/frontend/Components/Inputs/SwatchInput.tsx b/app/frontend/Components/Inputs/SwatchInput.tsx new file mode 100644 index 0000000..8a7e077 --- /dev/null +++ b/app/frontend/Components/Inputs/SwatchInput.tsx @@ -0,0 +1,41 @@ +import React, { forwardRef, useState } from 'react' +import HiddenInput from './HiddenInput' +import SwatchPicker from '../SwatchPicker' +import Label from './Label' +import { InputProps } from 'react-html-props' +import { type BaseInputProps } from '.' +import InputWrapper from './InputWrapper' + +export interface SwatchInputProps extends Omit, BaseInputProps { + label?: React.ReactNode + initialValue?: string + onChange?: (color: string) => void + wrapperProps?: Record +} + +const SwatchInput = forwardRef(( + { label, id, name, required, initialValue, onChange, wrapper, wrapperProps, ...props }, + ref, +) => { + const [color, setColor] = useState(initialValue || '') + + const handleChange = (color: string) => { + setColor(color) + + onChange?.(color) + } + + const inputId = id || name + + return ( + + { label && } + + + + ) +}) + +export default SwatchInput diff --git a/app/frontend/Components/Inputs/Switch.tsx b/app/frontend/Components/Inputs/Switch.tsx index 3bd1017..6c0c0ce 100644 --- a/app/frontend/Components/Inputs/Switch.tsx +++ b/app/frontend/Components/Inputs/Switch.tsx @@ -1,25 +1,31 @@ import React, { forwardRef } from 'react' import { Switch, type SwitchProps as MantineSwitchProps } from '@mantine/core' +import { type BaseInputProps } from '.' +import InputWrapper from './InputWrapper' -export interface SwitchProps extends MantineSwitchProps {} +export interface SwitchProps extends MantineSwitchProps, BaseInputProps {} const SwitchComponent = forwardRef(( - { id, name, style, ...props }, + { id, name, style, wrapper, wrapperProps, onClick, ...props }, ref, ) => { const inputId = id ?? name return ( - <> + { + e.stopPropagation() + onClick?.(e) + } } { ...props } /> - + ) }) diff --git a/app/frontend/Components/Inputs/TextInput.tsx b/app/frontend/Components/Inputs/TextInput.tsx index 794c3dd..3d7ceb5 100644 --- a/app/frontend/Components/Inputs/TextInput.tsx +++ b/app/frontend/Components/Inputs/TextInput.tsx @@ -1,17 +1,57 @@ -import React, { forwardRef } from 'react' +import React, { forwardRef, useState } from 'react' import { TextInput, type TextInputProps as MantineTextInputProps } from '@mantine/core' -import Label from '../Label' +import { type BaseInputProps } from '.' +import Label from './Label' +import InputWrapper from './InputWrapper' +import { CrossIcon } from '../Icons' +import { isUnset } from '@/lib' -export interface TextInputProps extends MantineTextInputProps {} +export interface TextInputProps extends MantineTextInputProps, BaseInputProps { + clearable?: boolean +} const TextInputComponent = forwardRef(( - { name, label, required = false, id, size = 'md', radius = 'xs', ...props }, + { + name, + label, + required = false, + id, + size = 'md', + wrapper, + wrapperProps, + clearable = false, + value, + onChange, + onClick, + readOnly, + ...props + }, ref, ) => { + // Manage value as a local state to enable clearable feature + const [localValue, setLocalValue] = useState(value || '') + + const handleChange = (e: React.ChangeEvent) => { + if(onChange) { + onChange(e) + } else { + setLocalValue(e.target.value) + } + } + + const handleClear = () => { + const fakeEvent = { + target: { + value: '', + }, + } as React.ChangeEvent + handleChange(fakeEvent) + } + const inputId = id || name return ( - <> + { label && } @@ -19,12 +59,18 @@ const TextInputComponent = forwardRef(( ref={ ref } name={ name } id={ inputId } + value={ value || localValue } + onChange={ handleChange } required={ required } size={ size } - radius={ radius } + rightSection={ !readOnly && clearable && !isUnset(value) && } + onClick={ e => { + e.stopPropagation() + onClick?.(e) + } } { ...props } /> - + ) }) diff --git a/app/frontend/Components/Inputs/Textarea.tsx b/app/frontend/Components/Inputs/Textarea.tsx index 25d058c..6bd0c61 100644 --- a/app/frontend/Components/Inputs/Textarea.tsx +++ b/app/frontend/Components/Inputs/Textarea.tsx @@ -1,17 +1,28 @@ import React, { forwardRef } from 'react' import { Textarea, type TextareaProps as MantineTextareaProps } from '@mantine/core' -import Label from '../Label' +import { type BaseInputProps } from '.' +import Label from './Label' +import InputWrapper from './InputWrapper' -export interface TextareaProps extends MantineTextareaProps { } +export interface TextareaProps extends MantineTextareaProps, BaseInputProps {} const TextareaComponent = forwardRef(( - { label, name, required = false, value, id, radius = 'xs', ...props }, + { + label, + name, + required = false, + id, + wrapper, + wrapperProps, + onClick, + ...props + }, ref, ) => { const inputId = id || name return ( - <> + { label && } @@ -19,13 +30,15 @@ const TextareaComponent = forwardRef(( ref={ ref } id={ inputId } name={ name } - value={ value ? String(value) : '' } required={ required } - radius={ radius } + onClick={ e => { + e.stopPropagation() + onClick?.(e) + } } { ...props } > - + ) }) diff --git a/app/frontend/Components/Inputs/index.ts b/app/frontend/Components/Inputs/index.ts index c00e265..bfefcbe 100644 --- a/app/frontend/Components/Inputs/index.ts +++ b/app/frontend/Components/Inputs/index.ts @@ -1,11 +1,23 @@ -export { default as TextInput } from './TextInput' -export { default as NumberInput } from './NumberInput' -export { default as Textarea } from './Textarea' -export { default as RichText } from './RichText' -export { default as PasswordInput } from './PasswordInput' -export { default as CurrencyInput } from './CurrencyInput' -export { default as HiddenInput } from './HiddenInput' -export { default as Checkbox } from './Checkbox' -export { default as RadioButtons } from './RadioButtons' -export { default as DateTime } from './DateTime' -export { default as Select } from './Select' +import { type DateValue, type DatesRangeValue } from '@mantine/dates' + +export { default as AutocompleteInput } from './AutocompleteInput' +export { default as Checkbox } from './Checkbox' +export { default as CurrencyInput } from './CurrencyInput' +export { default as DateInput } from './DateInput' +export { default as DateTimeInput } from './DateTimeInput' +export { default as HiddenInput } from './HiddenInput' +export { default as MultiSelect } from './MultiSelect' +export { default as NumberInput } from './NumberInput' +export { default as PasswordInput } from './PasswordInput' +export { default as SegmentedControl } from './SegmentedControl' +export { default as RichText } from './RichText' +export { default as Select } from './Select' +export { default as SwatchInput } from './SwatchInput' +export { default as Textarea } from './Textarea' +export { default as TextInput } from './TextInput' + +export interface BaseInputProps { + wrapper?: boolean +} + +export type DateInputValue = DateValue | DatesRangeValue | Date[] | undefined diff --git a/app/frontend/Components/Label/index.tsx b/app/frontend/Components/Label/index.tsx index 43a6935..c013439 100644 --- a/app/frontend/Components/Label/index.tsx +++ b/app/frontend/Components/Label/index.tsx @@ -1,13 +1,15 @@ import React from 'react' import { Box, type BoxProps } from '@mantine/core' import cx from 'clsx' -import { LabelProps } from 'react-html-props' -interface ILabelProps extends BoxProps, Omit { +interface LabelProps + extends BoxProps, + Omit, keyof BoxProps> +{ required?: boolean } -const Label = ({ children, required = false, className, ...props }: ILabelProps) => { +const Label = ({ children, required = false, className, ...props }: LabelProps) => { return ( { children } diff --git a/app/frontend/Components/Link/AnchorLink.tsx b/app/frontend/Components/Link/AnchorLink.tsx index 741d0e7..b4d1fae 100644 --- a/app/frontend/Components/Link/AnchorLink.tsx +++ b/app/frontend/Components/Link/AnchorLink.tsx @@ -2,13 +2,13 @@ import React, { forwardRef } from 'react' import { Link, type InertiaLinkProps } from '@inertiajs/react' import { Anchor, type AnchorProps } from '@mantine/core' -export interface IAnchorLinkProps +export interface AnchorLinkProps extends Omit, Omit { } -const AnchorLink = forwardRef(( +const AnchorLink = forwardRef(( props, ref, ) => { diff --git a/app/frontend/Components/Link/ButtonLink.tsx b/app/frontend/Components/Link/ButtonLink.tsx deleted file mode 100644 index 1598bd5..0000000 --- a/app/frontend/Components/Link/ButtonLink.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { forwardRef } from 'react' -import { Button, ButtonProps } from '@mantine/core' -import { Link } from '@inertiajs/react' - -interface IButtonLinkProps - extends ButtonProps, - Omit, 'color'|'size'|'style'> {} - -const ButtonLink = forwardRef((props, ref) => ( - - - - - { columns.filter(option => option.hideable).map(({ label, hideable }) => ( - - - - )) } - - - ) -} - -export default ColumnPicker diff --git a/app/frontend/Components/Table/Footer.tsx b/app/frontend/Components/Table/Footer.tsx index b41c5bf..2687c4d 100644 --- a/app/frontend/Components/Table/Footer.tsx +++ b/app/frontend/Components/Table/Footer.tsx @@ -1,15 +1,15 @@ import React, { forwardRef } from 'react' import { TableSectionContextProvider } from './TableContext' -import { Box, ElementProps, type BoxProps } from '@mantine/core' +import { Table, type TableTfootProps } from '@mantine/core' -interface ITableFoot extends BoxProps, ElementProps<'tfoot'> {} +interface TableFooterProps extends TableTfootProps {} -const Footer = forwardRef(({ children, ...props }, ref) => { +const Footer = forwardRef(({ children, ...props }, ref) => { return ( - + { children } - + ) }) diff --git a/app/frontend/Components/Table/Head.tsx b/app/frontend/Components/Table/Head.tsx index 6e24011..744c4e3 100644 --- a/app/frontend/Components/Table/Head.tsx +++ b/app/frontend/Components/Table/Head.tsx @@ -1,15 +1,21 @@ import React, { forwardRef } from 'react' import { TableSectionContextProvider } from './TableContext' -import { Box, ElementProps, type BoxProps } from '@mantine/core' +import { Table, type TableTheadProps } from '@mantine/core' -interface ITableHead extends BoxProps, ElementProps<'thead'> {} +interface TableHead extends TableTheadProps {} -const Head = forwardRef(({ children, ...props }, ref) => { +const Head = forwardRef(( + { children, ...props }, + ref, +) => { return ( - + { children } - + ) }) diff --git a/app/frontend/Components/Table/Pagination/LimitSelect.tsx b/app/frontend/Components/Table/Pagination/LimitSelect.tsx index c96601a..c1821a0 100644 --- a/app/frontend/Components/Table/Pagination/LimitSelect.tsx +++ b/app/frontend/Components/Table/Pagination/LimitSelect.tsx @@ -1,53 +1,82 @@ import React from 'react' import { router } from '@inertiajs/react' -import { Select } from '@mantine/core' +import { Select, type SelectProps } from '@mantine/core' +import { useLocation, usePageProps } from '@/lib/hooks' +import useLayoutStore from '@/lib/store/LayoutStore' +import { type Pagination } from '@/types' import axios from 'axios' import { Routes } from '@/lib' -import { useLocation, usePageProps } from '@/lib/hooks' -import classes from './Pagination.module.css' -interface ILimitSelectProps { - pagination: Schema.Pagination +import cx from 'clsx' +import * as classes from '../Table.css' + +interface LimitSelectProps extends SelectProps { + pagination: Pagination model: string } -const LimitSelect = ({ pagination, model }: ILimitSelectProps) => { +const LimitSelect = ({ pagination, model }: LimitSelectProps) => { const { auth: { user } } = usePageProps() const location = useLocation() + const defaultLimit = useLayoutStore(state => state.defaults.tableRecordsLimit) - const handleLimitChange = (limit: string) => { + const handleLimitChange = (limit: string|null) => { if(!model) return - // axios.patch( Routes.apiUpdateTablePreferences(user.id!), { - // user: { - // table_preferences: { - // [model]: { limit }, - // }, + limit ||= String(defaultLimit) + + // userTableLimitMutation.mutate({ + // id: user.id, + // preferences: { + // [model]: { limit }, + // }, + // }, { + // onSuccess: () => { + // // Redirect to first page if new limit puts page out of bounds of records + // if(parseInt(limit) * (pagination.current_page - 1) > pagination.count) { + // location.params.delete('page') + // router.get( + // location.path, + // { ...location.paramsAsJson }, + // { preserveScroll: true }, + // ) + // } else { + // router.reload() + // } // }, - // }).then(() => { - // // Redirect to first page if new limit puts page out of bounds of records - // if(parseInt(limit) * (pagination.current_page - 1) > pagination.count) { - // location.params.delete('page') - // router.get( - // location.path, - // { ...location.paramsAsJson }, - // { preserveScroll: true }, - // ) - // } else { - // router.reload() - // } // }) + + // TODO: Use react-query + axios.patch( Routes.apiUpdateTablePreferences(user.id!), { + user: { + table_preferences: { + [model]: { limit }, + }, + }, + }).then(() => { + // Redirect to first page if new limit puts page out of bounds of records + if(parseInt(limit) * (pagination.current_page - 1) > pagination.count) { + location.params.delete('page') + router.get( + location.path, + { ...location.paramsAsJson }, + { preserveScroll: true }, + ) + } else { + router.reload() + } + }) } return ( + ) +} + +export default React.memo(Type) diff --git a/app/frontend/Components/Table/SearchInput/AdvancedSearch/DateRangeInputs/index.ts b/app/frontend/Components/Table/SearchInput/AdvancedSearch/DateRangeInputs/index.ts new file mode 100644 index 0000000..13a5c6d --- /dev/null +++ b/app/frontend/Components/Table/SearchInput/AdvancedSearch/DateRangeInputs/index.ts @@ -0,0 +1,9 @@ +import useAdvancedSearch from '../useAdvancedSearch' + +export interface AdvancedInputProps { + advancedSearch: ReturnType + name: string +} + +export { default as SearchDateTypeInput } from './Type' +export { default as SearchDateInput } from './Date' diff --git a/app/frontend/Components/Table/SearchInput/AdvancedSearch/ValueRangeInputs/index.tsx b/app/frontend/Components/Table/SearchInput/AdvancedSearch/ValueRangeInputs/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/frontend/Components/Table/SearchInput/AdvancedSearch/buildSearchLink.ts b/app/frontend/Components/Table/SearchInput/AdvancedSearch/buildSearchLink.ts new file mode 100644 index 0000000..11ed519 --- /dev/null +++ b/app/frontend/Components/Table/SearchInput/AdvancedSearch/buildSearchLink.ts @@ -0,0 +1,57 @@ +import { NestedURLSearchParams, coerceArray, isUnset } from '@/lib' +import { type InputParam } from './useAdvancedSearch' + +/** + * Generate a URL for advanced searching + * + * @param inputParams List of all input params passed to hook + * @param values Map of all current search values + * @returns Link to same page with URL params to use for advanced search + */ +function buildSearchLink( + inputParams: readonly InputParam[], + values: NestedURLSearchParams, +) { + const localValues = values.clone() + + inputParams.forEach(param => { + const value = localValues.get(param.name) + + // Exclude key if dependents are empty + if(param?.dependent) { + let shouldBeIncluded = true + + coerceArray(param.dependent).forEach(dependentParam => { + if(isUnset(values.get(dependentParam))) { + shouldBeIncluded = false + } + }) + + if(!shouldBeIncluded) { + localValues.unset(param.name) + return + } + } + + // Handle Date values + if(value instanceof Date || (Array.isArray(value) && value[0] instanceof Date)) { + const dateStr = coerceArray(value).reduce((str, date, i) => { + return `${str}${i === 0 ? '' : ','}${date.toISOString()}` + }, '') + localValues.set(param.name, dateStr) + return + } + + localValues.set(param.name, value) + + }) + + if(localValues.isEmpty()) { + return `${location.pathname}` + } else { + localValues.set('adv', 'true') + return localValues.toString() + } +} + +export default buildSearchLink diff --git a/app/frontend/Components/Table/SearchInput/AdvancedSearch/index.tsx b/app/frontend/Components/Table/SearchInput/AdvancedSearch/index.tsx new file mode 100644 index 0000000..d69ece9 --- /dev/null +++ b/app/frontend/Components/Table/SearchInput/AdvancedSearch/index.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react' +import { DoubleDownArrowIcon } from '@/Components/Icons' +import { useLayoutStore } from '@/lib/store' +import { useBooleanToggle } from '@/lib/hooks' +import { useClickOutside } from '@mantine/hooks' +import { + ActionIcon, + Paper, + Transition, + useMantineTheme, + rem, + px, + Tooltip, + Box, +} from '@mantine/core' + +import cx from 'clsx' +import * as classes from './AdvancedSearch.css' + +const scaleY = { + in: { opacity: 1, transform: 'scaleY(1)' }, + out: { opacity: 0, transform: 'scaleY(0)' }, + common: { transformOrigin: 'top' }, + transitionProperty: 'transform, opacity', +} + +interface AdvancedSearchProps { + children: React.ReactNode +} + +const AdvancedSearch = ({ children }: AdvancedSearchProps) => { + const { sidebarOpen } = useLayoutStore() + const { primaryColor, other: { navbar: { width } } } = useMantineTheme() + const navBarWidth = width[sidebarOpen ? 'open' : 'closed'] + + const [open, toggleOpen] = useBooleanToggle(false) + const [searchButton, setSearchButton] = useState(null) + const [searchPaper, setSearchPaper] = useState(null) + + useClickOutside( + () => toggleOpen(false), + null, + [searchButton, searchPaper], + ) + + return ( + <> + + toggleOpen() } + ref={ setSearchButton } + data-ignore-outside-clicks + > + + + + + { (styles) => ( + + + { children } + + + ) } + + + ) +} + +export default AdvancedSearch + +export { default as useAdvancedSearch } from './useAdvancedSearch' diff --git a/app/frontend/Components/Table/SearchInput/AdvancedSearch/useAdvancedSearch.ts b/app/frontend/Components/Table/SearchInput/AdvancedSearch/useAdvancedSearch.ts new file mode 100644 index 0000000..aa4715e --- /dev/null +++ b/app/frontend/Components/Table/SearchInput/AdvancedSearch/useAdvancedSearch.ts @@ -0,0 +1,166 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useLocation } from '@/lib/hooks' +import { isUnset } from '@/lib/forms' +import { router } from '@inertiajs/react' +import cx from 'clsx' +import { NestedURLSearchParams } from '@/lib' +import buildSearchLink from './buildSearchLink' + +type SpecialSearchTypes = 'date' + +interface Options { + path: string +} + +export type InputParam = { + name: string + default?: T + dependent?: string|string[] + keyUpListener?: boolean + type?: SpecialSearchTypes +} + +export type ParamValue = string|number|Date|Date[]|undefined|null + +/** + * Hook for building advanced search interfaces + * @param inputParams Array of objects with the following structure: { label: string, name: string, default?: unknown, dependent?: string|string[] } + * @param options Options + * @returns link: A URL with the search parameters represented as GET params, + * reset(): A method to clear all search values, + * inputProps(name): Method to return object of props to be passed into an input + */ +const useAdvancedSearch = ( + inputParams: InputParam[], + options?: Options, +) => { + // TODO: Trying to infer keys from prop + type InputParamName = typeof inputParams[number]['name'] + + const location = useLocation() + + const [searchLink, setSearchLink] = useState(location.href) + + const localInputParams = useMemo(() => { + const finalParams: InputParam[] = [] + + inputParams.forEach(param => { + switch(param?.type) { + case 'date': + finalParams.push({ + name: `${param.name}[start]`, + }) + finalParams.push({ + name: `${param.name}[end]`, + }) + finalParams.push({ + name: `${param.name}[type]`, + dependent: `${param.name}[start]`, + }) + break + default: + finalParams.push(param) + return + } + }) + + return inputParams.concat(finalParams) + }, [inputParams]) + + // Builds a Map from keys in `inputParams` with values from URL string + // These are the starting values for local state used for form inputs + const startingValues = useMemo(() => localInputParams.reduce( + (data: NestedURLSearchParams, param) => { + // Handle special input types + switch(param?.type) { + case 'date': + data.set(`${param.name}[type]`, 'exact') + data.set(`${param.name}[start]`, data.get(`${param.name}[start]`) || '') + data.set(`${param.name}[end]`, data.get(`${param.name}[end]`) || '') + + return data + default: + data.set(param.name, data.get(param.name) || param.default || '') + } + + return data + }, + location.nestedParams.clone(), + ), [localInputParams, location.nestedParams]) + + const [values, setValues] = useState(startingValues) + + // Build URL params when input values change + useEffect(() => { + setSearchLink(buildSearchLink(localInputParams, values)) + }, [localInputParams, values]) + + const resetValues = useCallback(() => { + setValues(prevValues => localInputParams.reduce( + (data, param) => { + data.set(param.name, param.default ?? '') + return data + }, + prevValues.clone(), + )) + }, [localInputParams]) + + // Method returned from hook to be passed to an input + const buildInputProps = (name: InputParamName) => { + const param = localInputParams.find(param => param.name === name) + + let value: T + switch(param?.type) { + case 'date': + // @ts-ignore + value = new Date(values.get(name)) + break + default: + value = values.get(name) as T + } + + return { + name, + value, + mb: 10, + ...( param?.keyUpListener !== false && { onKeyUp: (e: React.KeyboardEvent) => { + if(e.key === 'Enter') { + router.get(searchLink, undefined, { preserveScroll: true }) + } + } }), + wrapperProps: { + className: cx({ highlighted: !isUnset(value) && !param?.dependent }), + // style: (theme: MantineTheme) => ({ + // '&.highlighted, &.highlighted input': { + // color: theme.other.colorSchemeOption( + // theme.colors[theme.primaryColor][6], + // theme.colors[theme.primaryColor][4], + // ), + // }, + // '&.highlighted input': { + // outlineColor: theme.other.colorSchemeOption( + // theme.colors[theme.primaryColor][6], + // theme.colors[theme.primaryColor][4], + // ), + // }, + // }), + }, + } + } + + const setInputValue = useCallback((name: InputParamName, value: ParamValue) => setValues(prevValues => { + const newValues = prevValues.clone() + newValues.set(name, value) + return newValues + }), []) + + return { + values, + link: searchLink, + inputProps: buildInputProps, + setInputValue, + reset: resetValues, + } +} + +export default useAdvancedSearch diff --git a/app/frontend/Components/Table/SearchInput/ColumnPicker.tsx b/app/frontend/Components/Table/SearchInput/ColumnPicker.tsx index 061fa3d..706e1b5 100644 --- a/app/frontend/Components/Table/SearchInput/ColumnPicker.tsx +++ b/app/frontend/Components/Table/SearchInput/ColumnPicker.tsx @@ -9,6 +9,9 @@ import { useTableContext } from '../TableContext' import { Button } from '@mantine/core' import { usePageProps } from '@/lib/hooks' +import cx from 'clsx' +import * as classes from '../Table.css' + const ColumnPicker = () => { const { auth: { user } } = usePageProps() const { tableState: { hideable, columns, model } } = useTableContext() @@ -16,7 +19,7 @@ const ColumnPicker = () => { if(!hideable || !model) return <> const handleChange = (e: React.ChangeEvent) => { - axios.patch( Routes.apiUpdateTablePreferences(user.id!), { + axios.patch( Routes.apiUpdateTablePreferences(user.id), { user: { table_preferences: { [model]: { @@ -34,7 +37,9 @@ const ColumnPicker = () => { return ( - + diff --git a/app/frontend/Components/Table/SearchInput/index.tsx b/app/frontend/Components/Table/SearchInput/index.tsx index a82d22c..fd6f398 100644 --- a/app/frontend/Components/Table/SearchInput/index.tsx +++ b/app/frontend/Components/Table/SearchInput/index.tsx @@ -8,10 +8,11 @@ import { SearchIcon, CrossIcon } from '@/Components/Icons' import { ActionIcon, Box } from '@mantine/core' import { useSessionStorage } from '@mantine/hooks' import ColumnPicker from './ColumnPicker' +import AdvancedSearch from './AdvancedSearch' import { useInit, useLocation } from '@/lib/hooks' import * as classes from '../Table.css' -interface ISearchInputProps { +interface SearchInputProps { columnPicker?: boolean advancedSearch?: React.ReactNode } @@ -20,7 +21,7 @@ interface ISearchInputProps { * Performs an Inertia request to the current url (window.location), using the search params * as query string with the key of 'search' */ -const SearchInput = ({ columnPicker = true, advancedSearch }: ISearchInputProps) => { +const SearchInput = ({ columnPicker = true, advancedSearch }: SearchInputProps) => { const { tableState: { model }, setTableState } = useTableContext() const location = useLocation() @@ -73,10 +74,10 @@ const SearchInput = ({ columnPicker = true, advancedSearch }: ISearchInputProps) (url.searchParams.get('search') === null && searchValue === '') ) return - if(!searchValue || searchValue === '') { + if(searchValue === '') { url.searchParams.delete('search') } else { - url.searchParams.set('search', searchValue) + url.searchParams.set('search', searchValue ?? '') url.searchParams.delete('page') } @@ -85,17 +86,20 @@ const SearchInput = ({ columnPicker = true, advancedSearch }: ISearchInputProps) return ( + { advancedSearch && { advancedSearch } } setSearchValue(e.target.value) } - rightSection={ searchValue !== '' && setSearchValue('') }> + rightSection={ searchValue !== '' && setSearchValue('') }> } - icon={ } + leftSection={ } + leftSectionPointerEvents="none" className={ classes.searchInput } aria-label="Search" + wrapper={ false } /> { columnPicker && } diff --git a/app/frontend/Components/Table/Section.tsx b/app/frontend/Components/Table/Section.tsx index e2e1154..afce17e 100644 --- a/app/frontend/Components/Table/Section.tsx +++ b/app/frontend/Components/Table/Section.tsx @@ -4,7 +4,7 @@ import * as classes from './Table.css' const TableSection = ({ children }: { children: React.ReactNode }) => { return ( -
+
{ children }
) diff --git a/app/frontend/Components/Table/Table.css.ts b/app/frontend/Components/Table/Table.css.ts index cbdbfb5..c485977 100644 --- a/app/frontend/Components/Table/Table.css.ts +++ b/app/frontend/Components/Table/Table.css.ts @@ -9,55 +9,8 @@ export const wrapper = css` max-height: 100%; ` -export const rowSpacing = css` - padding: ${vars.spacing.md}; - - border-collapse: separate; - border-spacing: 0 0.5em; - background-color: transparent !important; - - - tbody { - border-collapse: separate; - border-spacing: 0 0.5em; - - tr { - border-radius: ${vars.radius.lg}; - position: relative; - box-shadow: ${vars.shadows.xs}; - transition: box-shadow 0.2s ease-in-out; - - &:hover { - box-shadow: ${vars.shadows.sm}; - } - } - - td { - background-color: ${vars.colors.gray[0]}; - - &:first-of-type { - border-top-left-radius: ${vars.radius.lg}; - border-bottom-left-radius: ${vars.radius.lg}; - } - - &:last-of-type { - border-top-right-radius: ${vars.radius.lg}; - border-bottom-right-radius: ${vars.radius.lg}; - } - } - } -` - export const table = css` width: 100%; - - ${vars.lightSelector} { - background-color: ${vars.colors.gray[2]}; - } - - ${vars.darkSelector} { - background-color: ${vars.colors.dark[6]}; - } &.layout-fixed { table-layout: fixed; @@ -76,24 +29,12 @@ export const table = css` ${vars.lightSelector} { background-color: ${vars.colors.gray[1]}; - /* th:hover { + th:hover { background-color: ${vars.colors.gray[1]}; - } */ + } } ${vars.darkSelector} { background-color: ${vars.colors.dark[7]}; - - /* th:hover { - background-color: ${vars.colors.black}; - } */ - } - } - - &.${rowSpacing} thead { - background-color: ${vars.colors.white}; - - th { - border-right: 1px solid ${vars.colors.gray[2]} } } @@ -110,10 +51,7 @@ export const table = css` } } - th { - text-align: left; - &.sortable { position: relative; padding-right: 1rem; @@ -148,31 +86,35 @@ export const table = css` } + /* On small screens, collapse tables into "cards" */ @media(max-width: ${vars.breakpoints.sm}) { thead { display: none; } - tr { - display: flex; - flex-direction: column; - margin-bottom: 10px; - background-color: ${vars.colors.dark[7]}; - border-radius: ${rem(4)}; - padding: ${rem(6)}; - border-bottom: 1px solid ${vars.colors.primary.filled}; - } + /* Only for tables with a thead */ + thead + tbody { + tr { + display: flex; + flex-direction: column; + margin-bottom: 10px; + background-color: ${vars.colors.dark[7]}; + border-radius: ${rem(4)}; + padding: ${rem(6)}; + border-bottom: 1px solid ${vars.colors.primaryColors.filled}; + } - td { - display: grid; - grid-template-columns: 8rem 1fr; + td { + display: grid; + grid-template-columns: 8rem 1fr; - &::before { - content: attr(data-cell); - } + &::before { + content: attr(data-cell); + } - &.table-row-select-checkbox { - visibility: collapse; + &.table-row-select-checkbox { + visibility: collapse; + } } } } @@ -187,6 +129,7 @@ export const section = css` export const searchWrapper = css` display: flex; flex: 1; + width: 100%; ` export const searchInput = css` @@ -195,20 +138,23 @@ export const searchInput = css` input { border-top-right-radius: 0; border-bottom-right-radius: 0; + border-top-left-radius: ${vars.radius.sm}; + border-bottom-left-radius: ${vars.radius.sm}; } ` +export const columnPickerButton = css` + border-top-left-radius: 0; + border-bottom-left-radius: 0; +` -// &:before, &:after { -// position: absolute, -// display: block, -// right: 0.75rem, -// width: 0, -// height: 0, -// content: , -// cursor: pointer, -// border-color: vars.colors.gray[4], -// border-style: solid, -// borderLeft: `${theme.other.table.sortButtonHeight}px solid transparent !important`, -// borderRight: `${theme.other.table.sortButtonHeight}px solid transparent !important`, -// } +export const pagination = css` + a:hover { + text-decoration: none; + } +` + +export const limitSelect = css` + display: inline-block; + max-width: 60px; +` diff --git a/app/frontend/Components/Table/Table.tsx b/app/frontend/Components/Table/Table.tsx index a639466..24246ee 100644 --- a/app/frontend/Components/Table/Table.tsx +++ b/app/frontend/Components/Table/Table.tsx @@ -1,29 +1,28 @@ import React from 'react' -import { Table, type TableProps } from '@mantine/core' -import cx from 'clsx' -import * as classes from './Table.css' +import { Table, type TableProps as MantineTableProps } from '@mantine/core' import Head from './Head' import Body from './Body' import RowIterator from './RowIterator' import Row from './Row' -import Cell from './Cell' -import HeadCell from './Cell/HeadCell' +import Cell from './Td' +import HeadCell from './Th' import Footer from './Footer' import Pagination from './Pagination' -import TableProvider from './TableContext' +import TableProvider, { useTableContext } from './TableContext' import TableSection from './Section' import SearchInput from './SearchInput' -import ColumnPicker from './ColumnPicker' import ConditionalWrapper from '../ConditionalWrapper' -export interface ITableProps extends TableProps { +import cx from 'clsx' +import * as classes from './Table.css' + +export interface TableProps extends MantineTableProps { fixed?: boolean wrapper?: boolean - rowSpacing?: boolean } -type TableComponent = (({ children, className, fixed, wrapper, ...props }: ITableProps) => JSX.Element) +type TableComponent = ((props: TableProps) => JSX.Element) type TableObjects = { Head: typeof Head @@ -37,61 +36,55 @@ type TableObjects = { TableProvider: typeof TableProvider Section: typeof TableSection SearchInput: typeof SearchInput - ColumnPicker: typeof ColumnPicker } export type TableObject = TableComponent & TableObjects -const TableComponent: TableComponent & TableObjects = ({ +const TableComponent: TableObject = ({ children, className, wrapper = true, fixed = false, - rowSpacing = false, striped = true, highlightOnHover = true, + style, ...props }) => { - - // const stylesArray = useMemo(() => { - // const arr: (Sx | undefined)[] = [] - // if(wrapper) { - // arr.push({ thead: { top: -10 } }) - // } - // if(sx) { - // arr.push(...packSx(sx)) - // } - // return arr - // }, [wrapper, sx]) + const tableState = useTableContext(false) return (
{ children }
} > - { children } } > - { children } -
+ + { children } +
+
) } +TableComponent.TableProvider = TableProvider +TableComponent.Section = TableSection +TableComponent.SearchInput = SearchInput TableComponent.Head = Head +TableComponent.HeadCell = HeadCell TableComponent.Body = Body -TableComponent.RowIterator = RowIterator -TableComponent.Row = Row TableComponent.Cell = Cell -TableComponent.HeadCell = HeadCell +TableComponent.Row = Row +TableComponent.RowIterator = RowIterator TableComponent.Footer = Footer TableComponent.Pagination = Pagination -TableComponent.TableProvider = TableProvider -TableComponent.Section = TableSection -TableComponent.SearchInput = SearchInput -TableComponent.ColumnPicker = ColumnPicker export default TableComponent diff --git a/app/frontend/Components/Table/TableContext.tsx b/app/frontend/Components/Table/TableContext.tsx index ff2c160..a1bcd9e 100644 --- a/app/frontend/Components/Table/TableContext.tsx +++ b/app/frontend/Components/Table/TableContext.tsx @@ -1,23 +1,24 @@ import React, { useReducer, useEffect } from 'react' import { createContext } from '@/lib/hooks' +import { type Pagination } from '@/types' /** * Table Section Context * Used by Cell component to determine which tag to use */ -interface ITableSectionContextProvider { +interface TableSectionContextProvider { section: 'head'|'body'|'footer' } -const [useTableSectionContext, TableSectionContextProvider] = createContext() +const [useTableSectionContext, TableSectionContextProvider] = createContext() export { useTableSectionContext, TableSectionContextProvider } /** * Main Table Context */ -interface ITableSettings { +interface TableState { selectable: boolean - pagination?: Schema.Pagination + pagination?: Pagination rows?: Record[] columns: { hideable: string, label: string }[] selected: Set @@ -26,32 +27,37 @@ interface ITableSettings { searching: boolean } -interface ITableContext { - tableState: ITableSettings +interface TableContextValues { + tableState: TableState setTableState: Function } -interface ITableContextProviderProps { +interface TableContextProviderProps { children: React.ReactNode selectable?: boolean - pagination?: Schema.Pagination + pagination?: Pagination rows?: Record[] hideable?: boolean + + /** Name of the ActiveRecord model being tabularized. + * Used to limit Inertia props reload using `only`, needs to match the incoming prop on the Component to be effective. + * Also used as a key for User `table_preferences` to save hidden columns and pagination limit. + **/ model?: string } -const [useTableContext, TableContextProvider] = createContext() +const [useTableContext, TableContextProvider] = createContext() export { useTableContext } -const TableProvider: React.FC = ({ +const TableProvider = ({ children, selectable = false, pagination, rows = [], hideable = true, model, -}) => { - const tableReducer = (tableState: ITableSettings, newTableState: Partial) => ({ +}: TableContextProviderProps) => { + const tableReducer = (tableState: TableState, newTableState: Partial) => ({ ...tableState, ...newTableState, }) @@ -76,10 +82,10 @@ const TableProvider: React.FC = ({ ) } -interface IStatePreservingRowUpdaterProps { +interface StatePreservingRowUpdaterProps { children: React.ReactElement rows?: Record[] - pagination?: Schema.Pagination + pagination?: Pagination } /** @@ -87,14 +93,14 @@ interface IStatePreservingRowUpdaterProps { * Without this explicitly updating rows with the fresh data response, the table wouldn't update with new rows * This allows both sorting and filtering to work properly without losing input focus */ -const StatePreservingRowUpdater: React.FC = React.memo(({ children, rows, pagination }) => { +const StatePreservingRowUpdater = React.memo(({ children, rows, pagination }: StatePreservingRowUpdaterProps) => { const { setTableState } = useTableContext() useEffect(() => { if(pagination) { setTableState({ rows, pagination }) } - }, [rows, pagination]) + }, [rows, pagination, setTableState]) return <>{ children } }) diff --git a/app/frontend/Components/Table/Td/BodyCell.tsx b/app/frontend/Components/Table/Td/BodyCell.tsx new file mode 100644 index 0000000..a831f31 --- /dev/null +++ b/app/frontend/Components/Table/Td/BodyCell.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { useTableContext } from '../TableContext' +import BodyCellWithContext from './BodyCellWithContext' +import { Table } from '@mantine/core' +import { type TableCellProps } from '.' + +const BodyCell = ({ children, ...props }: TableCellProps) => { + const tableState = useTableContext(false) + + if(tableState === null) { + return { children } + } + + const { tableState: { model } } = tableState + + return + { children } + + +} + +export default BodyCell diff --git a/app/frontend/Components/Table/Td/BodyCellWithContext.tsx b/app/frontend/Components/Table/Td/BodyCellWithContext.tsx new file mode 100644 index 0000000..5ea7540 --- /dev/null +++ b/app/frontend/Components/Table/Td/BodyCellWithContext.tsx @@ -0,0 +1,32 @@ +import React, { useRef } from 'react' +import cx from 'clsx' +import { type TableCellProps } from '.' +import { Table } from '@mantine/core' + +export interface BodyCellWithContextProps extends Omit { + hideable?: false|string + model?: string +} + +const BodyCellWithContext = ({ + children, + fitContent, + hideable, + model, + className, + ...props +}: BodyCellWithContextProps) => { + const tdRef = useRef(null) + + return ( + + { children } + + ) +} + +export default BodyCellWithContext diff --git a/app/frontend/Components/Table/Td/index.tsx b/app/frontend/Components/Table/Td/index.tsx new file mode 100644 index 0000000..2c8bf10 --- /dev/null +++ b/app/frontend/Components/Table/Td/index.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { useTableContext } from '../TableContext' +import BodyCell from './BodyCell' +import { type TableTdProps } from '@mantine/core' +import { usePageProps } from '@/lib/hooks' + +export interface TableCellProps extends TableTdProps { + fitContent?: boolean + sort?: string + nowrap?: boolean + hideable?: false|string + ref?: React.RefObject +} + +const RenderedCell = ({ children = true, hideable, sort, ...props }: TableCellProps) => { + const { auth: { user: { table_preferences } } } = usePageProps() + + const tableState = useTableContext(false) + + let hiddenByUser: boolean = false + + if(tableState !== null) { + const { tableState: { model } } = tableState + + const hideableString = hideable || sort + if(hideableString !== undefined && model !== undefined) { + hiddenByUser = table_preferences?.[model]?.hide?.[hideableString] + } + } + + if(hiddenByUser) return <> + + return { children } +} + +export default RenderedCell diff --git a/app/frontend/Components/Table/Th/HeadCell.tsx b/app/frontend/Components/Table/Th/HeadCell.tsx new file mode 100644 index 0000000..86926aa --- /dev/null +++ b/app/frontend/Components/Table/Th/HeadCell.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { useTableContext } from '../TableContext' +import HeadCellWithContext from './HeadCellWithContext' +import { Table } from '@mantine/core' +import { type TableHeadCellProps } from '.' + +const HeadCell = ({ children, ...props }: TableHeadCellProps) => { + const tableState = useTableContext(false) + + if(tableState === null) { + return { children } + } + + const { tableState: { rows } } = tableState + + return ( + + { children } + + ) +} + +export default HeadCell diff --git a/app/frontend/Components/Table/Th/HeadCellWithContext.tsx b/app/frontend/Components/Table/Th/HeadCellWithContext.tsx new file mode 100644 index 0000000..fb369e3 --- /dev/null +++ b/app/frontend/Components/Table/Th/HeadCellWithContext.tsx @@ -0,0 +1,74 @@ +import React, { useMemo, useRef } from 'react' +import { Link, Flex } from '@/Components' +import cx from 'clsx' +import { type TableHeadCellProps } from '.' +import { useLocation } from '@/lib/hooks' +import { Table } from '@mantine/core' + +interface HeadCellWithContextProps extends TableHeadCellProps { + rows?: Record[] +} + +const HeadCellWithContext = ({ + children, + fitContent = false, + sort, + rows, + hideable, + ...props +}: HeadCellWithContextProps) => { + const thRef = useRef(null) + const { pathname, params } = useLocation() + + const localParams = new URLSearchParams(params) + + const paramsSort = localParams.get('sort') + const paramsDirection = localParams.get('direction') + + const direction = paramsSort === sort && paramsDirection === 'asc' ? 'desc' : 'asc' + + const showSortLink: boolean = sort !== undefined && rows!.length > 1 + + // Use URLSearchParams object to build sort link per head cell + const sortLink = useMemo(() => { + if(!showSortLink) return undefined + + if(sort === undefined) { + localParams.delete('sort') + return undefined + } + + localParams.set('sort', sort) + + localParams.set('direction', direction) + + return `${pathname}?${localParams.toString()}` + }, [showSortLink, sort, direction, pathname]) + + return ( + + + { showSortLink && sortLink ? + + { children } + + : + children + } + + + ) +} + +export default HeadCellWithContext diff --git a/app/frontend/Components/Table/Th/index.tsx b/app/frontend/Components/Table/Th/index.tsx new file mode 100644 index 0000000..97b0262 --- /dev/null +++ b/app/frontend/Components/Table/Th/index.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { useTableContext } from '../TableContext' +import HeadCell from './HeadCell' +import { type TableThProps } from '@mantine/core' +import { usePageProps } from '@/lib/hooks' + +export interface TableHeadCellProps extends TableThProps { + fitContent?: boolean + sort?: string + nowrap?: boolean + hideable?: false|string + ref?: React.RefObject +} + +const RenderedCell = ({ children = true, hideable, sort, ...props }: TableHeadCellProps) => { + const { auth: { user: { table_preferences } } } = usePageProps() + + const tableState = useTableContext(false) + + let hiddenByUser: boolean = false + + if(tableState !== null) { + const { tableState: { model } } = tableState + + const hideableString = hideable || sort + if(hideableString !== undefined && model !== undefined) { + hiddenByUser = table_preferences?.[model]?.hide?.[hideableString] + } + } + + if(hiddenByUser) return <> + + return { children } +} + +export default RenderedCell diff --git a/app/frontend/Components/Table/index.ts b/app/frontend/Components/Table/index.ts index daec6b3..e16925d 100644 --- a/app/frontend/Components/Table/index.ts +++ b/app/frontend/Components/Table/index.ts @@ -1,3 +1,5 @@ -import Table, { type TableObject } from './Table' +import Table, { type TableObject, type TableProps } from './Table' +export { default as useAdvancedSearch } from './SearchInput/AdvancedSearch/useAdvancedSearch' export default Table as TableObject +export { TableProps } diff --git a/app/frontend/Components/Tabs/VerticalNavLayout.tsx b/app/frontend/Components/Tabs/VerticalNavLayout.tsx index 8ac0f50..b7394a2 100644 --- a/app/frontend/Components/Tabs/VerticalNavLayout.tsx +++ b/app/frontend/Components/Tabs/VerticalNavLayout.tsx @@ -5,7 +5,6 @@ import { Paper, useMantineTheme } from '@mantine/core' import { useViewportSize, useLocation } from '@/lib/hooks' import { px } from '@/lib' - export type TTab = { name: string label: string @@ -76,4 +75,4 @@ const VerticalNavLayout = ({ children, tabs, title, routePrefix }: IVerticalNavL ) } -export default React.memo(VerticalNavLayout) +export default VerticalNavLayout diff --git a/app/frontend/Components/index.ts b/app/frontend/Components/index.ts index c2a2f4f..53a92d8 100644 --- a/app/frontend/Components/index.ts +++ b/app/frontend/Components/index.ts @@ -1,21 +1,20 @@ export { default as Button } from './Button' export { default as ConditionalWrapper } from './ConditionalWrapper' -export { default as Control } from '../Features/Control' export { default as DangerousHtml } from './DangerousHtml' export { default as Heading } from './Heading' -export { default as Link, NavLink } from './Link' +export { default as Link } from './Link' export { default as Menu } from './Menu' -export { default as Modal } from './Modal' export { default as Page, type PageProps } from './Page' export { default as RichTextEditor } from './RichTextEditor' export { default as Section } from './Section' -export { default as Table } from './Table' +export { default as Table, type TableProps } from './Table' export { default as Label } from './Label' export { default as Tabs } from './Tabs' // Export UI library components as a proxy to allow easy refactoring export { ActionIcon, + Affix, AppShell, Badge, Box, diff --git a/app/frontend/Features/AddControlsInterface/ButtonControl.tsx b/app/frontend/Features/AddControlsInterface/ButtonControl.tsx index c5f3d66..ee23082 100644 --- a/app/frontend/Features/AddControlsInterface/ButtonControl.tsx +++ b/app/frontend/Features/AddControlsInterface/ButtonControl.tsx @@ -1,8 +1,12 @@ import React from 'react' import { useDraggable } from '@dnd-kit/core' -import { Button } from '@mantine/core' +import { Button, ButtonProps } from '@mantine/core' -const ButtonControl = () => { +interface ButtonControlProps extends ButtonProps { + +} + +const ButtonControl = ({ ...props }: ButtonControlProps) => { const { attributes, listeners, setNodeRef, transform } = useDraggable({ id: 'button', }) @@ -13,7 +17,7 @@ const ButtonControl = () => { return (
- +
) } diff --git a/app/frontend/Features/Control/Button/index.tsx b/app/frontend/Features/Control/Button/index.tsx deleted file mode 100644 index 991b2fc..0000000 --- a/app/frontend/Features/Control/Button/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { forwardRef } from 'react' -import { Button } from '@/Components' -import { Routes } from '@/lib' -import axios from 'axios' -import { type ButtonProps } from '@mantine/core' -import { type ControlProps } from '..' -import { controlRoute, controlTitle } from '../lib' -import { EditIcon } from '@/Components/Icons' -import { modals } from '@mantine/modals' -import ControlForm from '@/Pages/Controls/Form' - -import * as classes from '../Control.css' - -interface ButtonControlProps extends ButtonProps, ControlProps {} - -const ButtonControl = forwardRef(( - { edit, control, ...props }, - ref, -) => { - const handleButtonClick = () => { - const route = controlRoute(control) - - if(edit || route === false) return - - axios.put(route) - } - - return ( - - ) -}) - -export default ButtonControl diff --git a/app/frontend/Features/IndexPageTemplate/TableTitleSection.tsx b/app/frontend/Features/IndexPageTemplate/TableTitleSection.tsx index 7a781aa..cc1a2c0 100644 --- a/app/frontend/Features/IndexPageTemplate/TableTitleSection.tsx +++ b/app/frontend/Features/IndexPageTemplate/TableTitleSection.tsx @@ -1,10 +1,12 @@ import React from 'react' import { useTableContext } from '@/Components/Table/TableContext' -import { Box, Title, Group, Divider } from '@mantine/core' +import { Title, Group, Divider } from '@mantine/core' import { Menu } from '@/Components' import { TrashIcon } from '@/Components/Icons' import { router } from '@inertiajs/react' -import * as classes from './IndexPage.css' +import { IconType } from 'react-icons' + +// import * as classes from './IndexPage.css' // TODO: Figure out correct type for icon export interface IIndexTableTitleSectionProps { @@ -14,7 +16,7 @@ export interface IIndexTableTitleSectionProps { menuOptions?: { label: string href: string - icon?: any + icon?: IconType }[] } @@ -29,11 +31,10 @@ const IndexTableTitleSection = ({ children, title, deleteRoute, menuOptions }: I } return ( - - - - { title } - + + + { title } + @@ -41,7 +42,7 @@ const IndexTableTitleSection = ({ children, title, deleteRoute, menuOptions }: I { menuOptions && menuOptions.map(({ label, href, icon }, index) => { const Icon = icon return ( - }> + }> { label } ) @@ -58,9 +59,8 @@ const IndexTableTitleSection = ({ children, title, deleteRoute, menuOptions }: I - { !!children && - { children } - } + + { !!children && children } ) } diff --git a/app/frontend/Features/IndexPageTemplate/index.tsx b/app/frontend/Features/IndexPageTemplate/index.tsx index c29f9dc..72a758a 100644 --- a/app/frontend/Features/IndexPageTemplate/index.tsx +++ b/app/frontend/Features/IndexPageTemplate/index.tsx @@ -1,14 +1,13 @@ import React from 'react' import { Page, Table } from '@/Components' import TableTitleSection, { IIndexTableTitleSectionProps } from './TableTitleSection' -import { type TBreadcrumb } from '@/Components/Breadcrumbs' +import { type Pagination } from '@/types' interface IIndexPageTemplateProps extends IIndexTableTitleSectionProps { model: string rows: Record[] - pagination: Schema.Pagination + pagination: Pagination search?: boolean - breadcrumbs?: TBreadcrumb[] advancedSearch?: React.ReactNode } @@ -19,15 +18,12 @@ const IndexPageTemplate = ({ rows, pagination, search = true, - breadcrumbs, menuOptions, advancedSearch, deleteRoute, }: IIndexPageTemplateProps) => { return ( - + { const { isLoggedIn } = useAuth() @@ -13,10 +14,15 @@ const AppLayout = ({ children }: { children: any }) => { - - - OSC Commands Interface + + + + + OSC Commands Interface + + + @@ -36,6 +42,7 @@ const AppLayout = ({ children }: { children: any }) => { } + diff --git a/app/frontend/Layouts/AuthLayout/AuthLayout.css.ts b/app/frontend/Layouts/AuthLayout/AuthLayout.css.ts index 5aa5df8..65afcb9 100644 --- a/app/frontend/Layouts/AuthLayout/AuthLayout.css.ts +++ b/app/frontend/Layouts/AuthLayout/AuthLayout.css.ts @@ -15,6 +15,6 @@ export const authLayout = css` } #auth-layout-right { - background-color: ${vars.colors.primary.filled}; + background-color: ${vars.colors.primaryColors.filled}; } ` diff --git a/app/frontend/Layouts/Providers/UiFrameworkProvider.tsx b/app/frontend/Layouts/Providers/UiFrameworkProvider.tsx index 8947efd..53a7037 100644 --- a/app/frontend/Layouts/Providers/UiFrameworkProvider.tsx +++ b/app/frontend/Layouts/Providers/UiFrameworkProvider.tsx @@ -58,8 +58,8 @@ const UiFrameworkProvider = ({ children }: { children: React.ReactNode }) => { defaultColorScheme="dark" cssVariablesResolver={ cssVariablesResolver } > + - { children } diff --git a/app/frontend/Layouts/Providers/index.tsx b/app/frontend/Layouts/Providers/index.tsx index c799aaf..625e06c 100644 --- a/app/frontend/Layouts/Providers/index.tsx +++ b/app/frontend/Layouts/Providers/index.tsx @@ -5,6 +5,7 @@ import UiFrameworkProvider from './UiFrameworkProvider' import './reset.css' import '@mantine/core/styles.css' import '@mantine/tiptap/styles.css' +import '@mantine/notifications/styles.css' import './global.css' import QueryProvider from './QueryProvider' @@ -12,7 +13,7 @@ interface IProviderProps { children?: React.ReactNode } -const Providers = React.memo(({ children }: IProviderProps) => { +const Providers = ({ children }: IProviderProps) => { return ( @@ -22,6 +23,6 @@ const Providers = React.memo(({ children }: IProviderProps) => { ) -}) +} export default Providers diff --git a/app/frontend/Layouts/index.tsx b/app/frontend/Layouts/index.tsx index 9992d25..b8190d9 100644 --- a/app/frontend/Layouts/index.tsx +++ b/app/frontend/Layouts/index.tsx @@ -24,14 +24,14 @@ interface InertiaPageProps extends PageProps { props: LayoutWrapperProps } -const LayoutWrapper = React.memo(({ children }: LayoutWrapperProps) => { +const LayoutWrapper = ({ children }: LayoutWrapperProps) => { return ( { children } ) -}) +} const AppLayoutLayout = (page: InertiaPageProps) => { return ( diff --git a/app/frontend/Pages/Commands/Form.tsx b/app/frontend/Pages/Commands/Form.tsx index bd107fa..3e678f2 100644 --- a/app/frontend/Pages/Commands/Form.tsx +++ b/app/frontend/Pages/Commands/Form.tsx @@ -1,19 +1,19 @@ import React from 'react' import { Grid, Text } from '@/Components' -import { Form, TextInput, Submit, RichText, DynamicInputs, Checkbox } from '@/Components/Form' -import { type UseFormProps } from 'use-inertia-form' +import { Form, TextInput, Submit, RichText, Checkbox, DynamicInputs } from '@/Components/Form' +import { type HTTPVerb, type UseFormProps } from 'use-inertia-form' import { CommandPayloadTypesDropdown, ServerDropdown } from '@/Components/Dropdowns' import { exclude } from '@/lib' import { useListState } from '@mantine/hooks' -type TCommandFormData = { +type CommandFormData = { command: Schema.CommandsFormData } -export interface ICommandFormProps { +export interface CommandFormProps { to: string method?: HTTPVerb - onSubmit?: (object: UseFormProps) => boolean|void + onSubmit?: (object: UseFormProps) => boolean|void command?: Schema.CommandsFormData } @@ -27,7 +27,7 @@ const emptyCommand: Partial = { command_values: [], } -const CommandForm = ({ method = 'post', command, ...props }: ICommandFormProps) => { +const CommandForm = ({ method = 'post', command, ...props }: CommandFormProps) => { const [deletedValues, deletedValueHandlers] = useListState<{ id: string|number, _destroy: boolean}>() const handleRemoveCommandValue = (record: Schema.CommandValue) => { diff --git a/app/frontend/Pages/Commands/Index/index.tsx b/app/frontend/Pages/Commands/Index/index.tsx index f0ca22a..75edab9 100644 --- a/app/frontend/Pages/Commands/Index/index.tsx +++ b/app/frontend/Pages/Commands/Index/index.tsx @@ -3,10 +3,11 @@ import { Routes } from '@/lib' import { IndexPageTemplate } from '@/Features' import { NewIcon } from '@/Components/Icons' import CommandsTable from '../Table' +import { type Pagination } from '@/types' interface ICommandIndexProps { commands: Schema.CommandsIndex[] - pagination: Schema.Pagination + pagination: Pagination } const CommandsIndex = ({ commands, pagination }: ICommandIndexProps) => { @@ -21,7 +22,7 @@ const CommandsIndex = ({ commands, pagination }: ICommandIndexProps) => { { label: 'New Command', href: Routes.newCommand(), icon: NewIcon }, ] } > - + ) } diff --git a/app/frontend/Pages/Commands/Show/index.tsx b/app/frontend/Pages/Commands/Show/index.tsx index 8c8eede..67f8d23 100644 --- a/app/frontend/Pages/Commands/Show/index.tsx +++ b/app/frontend/Pages/Commands/Show/index.tsx @@ -1,13 +1,13 @@ import React from 'react' import { Box, Code, DangerousHtml, Group, Heading, Link, Menu, Page, Section } from '@/Components' import { Routes } from '@/lib' -import ButtonControl from '@/Features/Control/Button' +import ButtonControl from '@/Pages/Screens/Components/Control/Button' -interface IShowCommandProps { +interface ShowCommandProps { command: Schema.CommandsShow } -const ShowCommand = ({ command }: IShowCommandProps) => { +const ShowCommand = ({ command }: ShowCommandProps) => { const title = command.title ?? 'Command' return ( @@ -27,18 +27,22 @@ const ShowCommand = ({ command }: IShowCommandProps) => {
- Server: { command.server.title } + Server: + + { command.server.title } + + Address String: { command.address } Payload Type: { command.payload_type } { command.description } - { command?.command_values && <> + { /* { command?.command_values && <> Test: { command.command_values?.map(value => ( )) } - } + } */ } ) } diff --git a/app/frontend/Pages/Commands/Table.tsx b/app/frontend/Pages/Commands/Table.tsx index c0dd680..1a1fe8a 100644 --- a/app/frontend/Pages/Commands/Table.tsx +++ b/app/frontend/Pages/Commands/Table.tsx @@ -1,20 +1,20 @@ import React from 'react' import { Routes } from '@/lib' -import { Table, Link } from '@/Components' +import { Table, Link, type TableProps } from '@/Components' import { DeleteButton, EditButton } from '@/Components/Button' -import { type ITableProps } from '@/Components/Table/Table' -const CommandTable = (props: ITableProps) => { +const CommandTable = (props: TableProps) => { return ( - Title - Address - Payload - Actions + Title + Address + Payload + Actions + ( diff --git a/app/frontend/Pages/Controls/Edit/index.tsx b/app/frontend/Pages/Controls/Edit/index.tsx deleted file mode 100644 index 4a836b8..0000000 --- a/app/frontend/Pages/Controls/Edit/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import { Heading, Page, Section } from '@/Components' -import { Routes } from '@/lib' -import ControlsForm from '../Form' - -interface IEditControlProps { - control: Schema.ControlsEdit -} - -const EditControl = ({ control }: IEditControlProps) => { - const title = 'Edit Control' - - return ( - -
- { title } - - -
-
- ) -} - -export default EditControl diff --git a/app/frontend/Pages/Controls/Form.tsx b/app/frontend/Pages/Controls/Form.tsx deleted file mode 100644 index 3034519..0000000 --- a/app/frontend/Pages/Controls/Form.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import { Code, Paper, ScrollArea, Text } from '@/Components' -import { Form, TextInput, Submit } from '@/Components/Form' -import { ProtocolDropdown } from '@/Components/Dropdowns' -import { useGetProtocol } from '@/queries' -import { FormProps } from 'use-inertia-form' - -type TControlFormData = { - control: Partial -} - -export interface ControlFormProps extends Omit, 'data'> { - control?: Schema.ControlsFormData -} - -const emptyControl: Partial = { - title: '', - control_type: undefined, - order: undefined, - value: undefined, - min_value: undefined, - max_value: undefined, -} - -const ControlForm = ({ method = 'post', control, ...props }: ControlFormProps) => { - let protocol = control?.protocol - - return ( -
- - { control?.control_type === 'slider' && <> - - - } - - { - console.log({ protocol, data: form.data }) - // const thing = useGetProtocol(control.slug) - } } /> - - { protocol && { protocol?.title } commands: } - - { protocol?.commands?.map(command => ( - { command.address } - )) - } - - - { control?.id ? 'Update' : 'Create' } Control - - ) -} - -export default ControlForm diff --git a/app/frontend/Pages/Controls/Index/index.tsx b/app/frontend/Pages/Controls/Index/index.tsx deleted file mode 100644 index d2638ca..0000000 --- a/app/frontend/Pages/Controls/Index/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import { Routes } from '@/lib' -import { IndexPageTemplate } from '@/Layouts/AppLayout/Components' -import { NewIcon } from '@/Components/Icons' -import ControlsTable from '../Table' - -interface IControlIndexProps { - controls: Schema.ControlsIndex[] - pagination: Schema.Pagination -} - -const ControlsIndex = ({ controls, pagination }: IControlIndexProps) => { - return ( - - - - ) -} - -export default ControlsIndex diff --git a/app/frontend/Pages/Controls/New/index.tsx b/app/frontend/Pages/Controls/New/index.tsx deleted file mode 100644 index 48c4a8e..0000000 --- a/app/frontend/Pages/Controls/New/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import { Heading, Page, Section } from '@/Components' -import { Routes } from '@/lib' -import ControlForm from '../Form' - -interface INewControlProps { - control: Schema.ControlsFormData -} - -const NewControl = ({ ...data }: INewControlProps) => { - const title = 'New Control' - - return ( - - -
- { title } - - -
- -
- ) -} - -export default NewControl diff --git a/app/frontend/Pages/Controls/Show/index.tsx b/app/frontend/Pages/Controls/Show/index.tsx deleted file mode 100644 index 7a851a6..0000000 --- a/app/frontend/Pages/Controls/Show/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' -import { Group, Heading, Menu, Page, Section } from '@/Components' -import { Routes } from '@/lib' - -interface IShowControlProps { - control: Schema.ControlsShow -} - -const ShowControl = ({ control }: IShowControlProps) => { - const title = 'Control' - - return ( - -
- - { title } - - - - - - Edit Control - - - - - -
-
- ) -} - -export default ShowControl diff --git a/app/frontend/Pages/Controls/Table.tsx b/app/frontend/Pages/Controls/Table.tsx deleted file mode 100644 index bae5b92..0000000 --- a/app/frontend/Pages/Controls/Table.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import { Routes } from '@/lib' -import { Table, Link } from '@/Components' -import { EditButton } from '@/Components/Button' -import { type ITableProps } from '@/Components/Table/Table' - -const ControlTable = (props: ITableProps) => { - return ( -
- - - Title - Type - - Position - Min_value - Max_value - Value - - Actions - - - - ( - - - { control.title } - - - { control.type } - - - - { control.position } - - - { control.min_value } - - - { control.max_value } - - - { control.value } - - - - - - - ) } /> - -
- ) -} - -export default ControlTable diff --git a/app/frontend/Pages/Devise/Login/index.tsx b/app/frontend/Pages/Devise/Login/index.tsx index 8c3f055..df39312 100644 --- a/app/frontend/Pages/Devise/Login/index.tsx +++ b/app/frontend/Pages/Devise/Login/index.tsx @@ -22,11 +22,8 @@ const defaultData = { } const Login = () => { - const emailInputRef = useRef(null) - const handleSubmit = ({ data }: UseFormProps) => { if(data.user.email === '' || data.user.password === '') { - emailInputRef.current!.focus() return false } } @@ -51,7 +48,6 @@ const Login = () => { autoFocus autoComplete="Email" required - ref={ emailInputRef } pattern=".+@.+\..+" /> diff --git a/app/frontend/Pages/Devise/Register/index.tsx b/app/frontend/Pages/Devise/Register/index.tsx index 610193b..52cc17d 100644 --- a/app/frontend/Pages/Devise/Register/index.tsx +++ b/app/frontend/Pages/Devise/Register/index.tsx @@ -14,7 +14,6 @@ type TRegisterFormData = { const Register = () => { const handleFormChange = ({ data }: UseFormProps) => { - // console.log({ data }) } const handlePasswordChange = (value: string|number, { data, getError, clearErrors }: UseFormProps) => { @@ -34,7 +33,6 @@ const Register = () => { } const handleEmailBlur = (value: string|number, form: UseFormProps) => { - // console.log({ value, form }) } return ( diff --git a/app/frontend/Pages/Pages/Dev/index.tsx b/app/frontend/Pages/Pages/Dev/index.tsx deleted file mode 100644 index 96179f3..0000000 --- a/app/frontend/Pages/Pages/Dev/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import { SortableList } from '@/Components/Sortable' -import { Box, Flex } from '@mantine/core' - -const Dev = ({ protocol }: { protocol: Schema.ProtocolsShow }) => { - return ( -
- - items={ protocol.commands } - >{ item => ( - - - - { item.title } - { item.order } - - - ) } - -
- ) -} - -export default Dev diff --git a/app/frontend/Pages/Protocols/Edit/index.tsx b/app/frontend/Pages/Protocols/Edit/index.tsx index 44cf826..5b20c87 100644 --- a/app/frontend/Pages/Protocols/Edit/index.tsx +++ b/app/frontend/Pages/Protocols/Edit/index.tsx @@ -2,7 +2,6 @@ import React from 'react' import { Heading, Page, Section } from '@/Components' import { Routes } from '@/lib' import ProtocolsForm from '../Form' -import { Select } from '@/Components/Inputs' interface IEditProtocolProps { protocol: Schema.ProtocolsEdit diff --git a/app/frontend/Pages/Protocols/Form/CommandInputs.tsx b/app/frontend/Pages/Protocols/Form/CommandInputs.tsx index 83dbd04..53d2f05 100644 --- a/app/frontend/Pages/Protocols/Form/CommandInputs.tsx +++ b/app/frontend/Pages/Protocols/Form/CommandInputs.tsx @@ -1,10 +1,11 @@ import React, { useMemo } from 'react' -import { Grid } from '@/Components' +import { Grid, Label } from '@/Components' import { NumberInput, TextInput, useDynamicInputContext } from '@/Components/Form' import { CommandDropdown, CommandValueDropdown } from '@/Components/Dropdowns' import { useForm } from 'use-inertia-form' import dayjs from 'dayjs' import { humanizeDuration } from '@/lib/formatters' +import TextInputComponent from '@/Components/Inputs/TextInput' interface CommandInputsProps { commands: Schema.CommandsOptions[] @@ -25,12 +26,14 @@ const CommandInputs = ({ commands }: CommandInputsProps) => { return ( + + { activeCommand ? { } + - { - (record?.delay || 0) === 0 ? - 'No Delay' - : - humanizeDuration(dayjs.duration(record?.delay || 0, 'millisecond')) - } + + + + + + ) } diff --git a/app/frontend/Pages/Protocols/Form/SortableFormSection.tsx b/app/frontend/Pages/Protocols/Form/SortableFormSection.tsx index e5b7da2..d2ef1a5 100644 --- a/app/frontend/Pages/Protocols/Form/SortableFormSection.tsx +++ b/app/frontend/Pages/Protocols/Form/SortableFormSection.tsx @@ -1,10 +1,6 @@ import React, { useMemo, useState } from 'react' -import type { ReactNode } from 'react' import { DndContext, - KeyboardSensor, - PointerSensor, - TouchSensor, useSensor, useSensors, } from '@dnd-kit/core' @@ -12,14 +8,13 @@ import type { Active, DragEndEvent, DragStartEvent, UniqueIdentifier } from '@dn import { SortableContext, arrayMove, - sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable' // import * as classes from './SortableList.css' import { useForm } from 'use-inertia-form' import { createContext } from '@/lib/hooks' -import { FormPointerSensor, FormTouchSensor } from '@/Components/Sortable' +import { FormPointerSensor } from '@/Components/Sortable' const [useSortableFormContext, SortableFormContextProvider] = createContext() export { useSortableFormContext } diff --git a/app/frontend/Pages/Protocols/Form/index.tsx b/app/frontend/Pages/Protocols/Form/index.tsx index 4eeb559..9eb4e7a 100644 --- a/app/frontend/Pages/Protocols/Form/index.tsx +++ b/app/frontend/Pages/Protocols/Form/index.tsx @@ -1,10 +1,10 @@ import React from 'react' -import { Form, TextInput, Submit, Textarea } from '@/Components/Form' -import { type UseFormProps } from 'use-inertia-form' +import { Form, TextInput, Submit, RichText } from '@/Components/Form' +import { type HTTPVerb, type UseFormProps } from 'use-inertia-form' import { Grid } from '@/Components' import CommandInputs from './CommandInputs' import { useGetCommands } from '@/queries' -import SortableDynamicInputs from '@/Components/Form/DynamicInputs/SortableDynamicInputs' +import SortableDynamicInputs from '@/Components/Form/Components/DynamicInputs/SortableDynamicInputs' type ProtocolFormData = { protocol: Schema.ProtocolsFormData @@ -18,7 +18,9 @@ export interface IProtocolFormProps { } const ProtocolForm = ({ method = 'post', protocol, ...props }: IProtocolFormProps) => { - const { data: commands } = useGetCommands({ initialData: protocol.commands }) + const { data: commands } = useGetCommands({ + initialData: protocol.commands as Schema.CommandsEdit[], + }) return (
-