diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..70e88db6 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + ], + overrides: [ + { + env: { + node: true, + }, + files: ['.eslintrc.{js,cjs}'], + parserOptions: { + sourceType: 'script', + }, + }, + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['@typescript-eslint', 'react'], + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + 'no-extra-semi': 'warn', + }, +} diff --git a/e2e/vite_example/src/App.tsx b/e2e/vite_example/src/App.tsx index e9107086..0c29c08e 100644 --- a/e2e/vite_example/src/App.tsx +++ b/e2e/vite_example/src/App.tsx @@ -150,6 +150,7 @@ function App() { }, forms: { GMI: RequiredFieldsForm, + GMIAllRequired: RequiredFieldsForm, }, itemsColumn: { Container: () => { diff --git a/guillotina_example/guillotina_react_app/guillotina_react_app/__init__.py b/guillotina_example/guillotina_react_app/guillotina_react_app/__init__.py index 22b8cfb0..e05493c9 100644 --- a/guillotina_example/guillotina_react_app/guillotina_react_app/__init__.py +++ b/guillotina_example/guillotina_react_app/guillotina_react_app/__init__.py @@ -22,4 +22,5 @@ def includeme(root): configure.scan("guillotina_react_app.vocabularies") configure.scan("guillotina_react_app.gmi") configure.scan("guillotina_react_app.gmi_behaviors") + configure.scan("guillotina_react_app.gmi_required") configure.scan("guillotina_react_app.workflow") diff --git a/guillotina_example/guillotina_react_app/guillotina_react_app/gmi/interface.py b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi/interface.py index d09872a1..a9bc1958 100644 --- a/guillotina_example/guillotina_react_app/guillotina_react_app/gmi/interface.py +++ b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi/interface.py @@ -10,8 +10,23 @@ "type": "object", "properties": { "items": { - "type": "array" - } + "type": "array", + "title": "Array in json" + }, + "text": { + "type": "string", + "title": "Text in json" + }, + "second_level": { + "type": "object", + "title": "Two levels", + "properties": { + "first_item_second_level": { + "type": "string", + "title": "Item second level text", + }, + }, + }, } } ) @@ -25,6 +40,9 @@ class IGMI(interfaces.IFolder): index_field("text_field", type="searchabletext") text_field = schema.Text(title="Text field", required=False) + index_field("textarea_field", type="searchabletext") + textarea_field = schema.Text(title="Text area field", required=False, widget="textarea") + text_line_field = schema.TextLine(title="Text line field") index_field("number_field", type="int") diff --git a/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/__init__.py b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/__init__.py new file mode 100644 index 00000000..556bc24f --- /dev/null +++ b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/__init__.py @@ -0,0 +1,2 @@ +from . import content +from . import interface \ No newline at end of file diff --git a/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/content.py b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/content.py new file mode 100644 index 00000000..fc6f5ceb --- /dev/null +++ b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/content.py @@ -0,0 +1,18 @@ +from guillotina import configure +from guillotina import content +from guillotina_react_app.gmi_required.interface import IGMIAllRequired + +@configure.contenttype( + type_name="GMIAllRequired", + schema=IGMIAllRequired, + behaviors=[ + "guillotina.behaviors.dublincore.IDublinCore", + "guillotina.behaviors.attachment.IAttachment", + "guillotina.contrib.image.behaviors.IImageAttachment", + "guillotina.contrib.workflows.interfaces.IWorkflowBehavior", + "guillotina.contrib.image.behaviors.IMultiImageOrderedAttachment", + ], + add_permission="guillotina.AddContent" +) +class GMIAllRequired(content.Folder): + pass \ No newline at end of file diff --git a/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/interface.py b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/interface.py new file mode 100644 index 00000000..a121cd84 --- /dev/null +++ b/guillotina_example/guillotina_react_app/guillotina_react_app/gmi_required/interface.py @@ -0,0 +1,119 @@ +import json +from guillotina import interfaces +from guillotina import schema +from guillotina.directives import index_field +from guillotina.fields import CloudFileField + +JSON_EXAMPLE_SCHEMA = json.dumps( + { + "title": "My Json Field", + "type": "object", + "properties": { + "items": { + "type": "array", + "title": "Array in json" + }, + "text": { + "type": "string", + "title": "Text in json" + }, + "second_level": { + "type": "object", + "title": "Two levels", + "properties": { + "first_item_second_level": { + "type": "string", + "title": "Item second level text", + }, + }, + }, + } + } +) +class IGMIAllRequired(interfaces.IFolder): + + json_example = schema.JSONField(schema=JSON_EXAMPLE_SCHEMA, required=False) + + index_field("text_richtext_field", type="searchabletext") + text_richtext_field = schema.Text(title="Text richtext field", required=True, widget="richtext") + + index_field("text_field", type="searchabletext") + text_field = schema.Text(title="Text field", required=True) + + index_field("textarea_field", type="searchabletext") + textarea_field = schema.Text(title="Text area field", required=True, widget="textarea") + + + text_line_field = schema.TextLine(title="Text line field", required=True) + index_field("number_field", type="int") + number_field = schema.Int(title="Number field", required=True) + index_field("boolean_field", type="boolean") + boolean_field = schema.Bool(title="Boolean field") + cloud_file_field = CloudFileField(title="Cloud file field") + list_field = schema.List(title="List field", value_type=schema.TextLine(), missing_value=[], required=True) + + index_field("datetime_field", type="date") + datetime_field = schema.Datetime(title="Datetime field", required=True) + + index_field("time_field", type="text") + time_field = schema.Time(title="Time field", required=True) + + index_field("date_field", type="date") + date_field = schema.Date(title="Date field", required=True) + + index_field("choice_field_vocabulary", type="keyword") + choice_field_vocabulary = schema.Choice( + title="Choice field vocabulary", + vocabulary="gmi_vocabulary", + required=True + ) + + index_field("choice_field", type="keyword") + choice_field = schema.Choice( + title="Choice field", + values=["date", "integer", "text", "float", "keyword", "boolean"], + required=True, + ) + + index_field("multiple_choice_field", type="keyword") + multiple_choice_field = schema.List( + title="Multiple choice field", + value_type=schema.Choice( + title="Choice field", + values=["date", "integer", "text", "float", "keyword", "boolean"], + ), + missing_value=[], + required=True + ) + + index_field("multiple_choice_field_vocabulary", type="keyword") + multiple_choice_field_vocabulary = schema.List( + title="Multiple choice field vocabulary", + value_type=schema.Choice( + title="Choice field vocabulary", + vocabulary="gmi_vocabulary", + required=True, + ), + missing_value=[], + required=True + ) + + gmi_ids = schema.List( + title="GMI list", + value_type=schema.TextLine(), + default=[], + null=True, + blank=True, + widget="search_list", + labelProperty="title", + typeNameQuery="GMI", + required=True + ) + + brother_gmi = schema.Text( + title="Brother GMI", + widget="search", + typeNameQuery="GMI", + labelProperty="title", + required=True + ) diff --git a/package.json b/package.json index b1c00ca5..cf24a291 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.27.0", + "version": "0.28.0", "repository": { "type": "git", "url": "git@github.com:guillotinaweb/guillotina_react.git" @@ -7,7 +7,7 @@ "files": [ "dist" ], - "source": "./src/guillo-gmi/index.js", + "source": "./src/guillo-gmi/index.ts", "main": "./dist/react-gmi.js", "exports": "./dist/react-gmi.modern.js", "types": "./dist/index.d.ts", @@ -31,32 +31,39 @@ "@babel/cli": "7.12.10", "@babel/core": "7.12.10", "@formatjs/cli": "^6.2.4", + "@formatjs/ts-transformer": "^3.13.11", "@testing-library/jest-dom": "5.11.6", "@testing-library/react": "11.2.2", "@testing-library/user-event": "12.6.0", + "@typescript-eslint/eslint-plugin": "^6.18.1", + "@typescript-eslint/parser": "^6.18.1", "babel-plugin-formatjs": "^10.5.10", + "eslint": "^8.56.0", + "eslint-plugin-react": "^7.33.2", "husky": "4.3.6", "microbundle": "0.13.0", "prettier": "2.2.1", "sass": "1.69.5", "serialize-javascript": "5.0.1", - "vitest": "^0.34.6" + "typescript": "^5.3.3", + "vitest": "^0.34.6", + "@types/react-beautiful-dnd": "13.1.8" }, "scripts": { - "format": "prettier --write \"src/**/*.js\"", + "format": "prettier --write \"src/**/*.{js,ts,tsx}\"", "format:tests": "prettier --write \"e2e/cypress/**/*.js\"", "format:check": "prettier --check \"src/**/*.js\"", "build": "yarn build:js && yarn build:css", "build:js": "rm -rf ./dist && microbundle --jsx React.createElement --no-compress --sourcemap", - "build:css": "rm -rf ./dist/css && mkdir ./dist/css && sass ./src/guillo-gmi/scss/styles.sass ./dist/css/style.css", + "build:css": "rm -rf ./dist/css && mkdir -p ./dist/css && sass ./src/guillo-gmi/scss/styles.sass ./dist/css/style.css", "prepublish": "yarn build", "test": "vitest run", + "lint": "eslint src", "intl-extract": "formatjs extract 'src/**/*.js' --out-file src/guillo-gmi/locales/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'", "intl-compile-en": "formatjs compile src/guillo-gmi/locales/en.json --ast --out-file src/guillo-gmi/locales/compiled/en.json", "intl-compile-ca": "formatjs compile src/guillo-gmi/locales/ca.json --ast --out-file src/guillo-gmi/locales/compiled/ca.json", "intl-compile-es": "formatjs compile src/guillo-gmi/locales/es.json --ast --out-file src/guillo-gmi/locales/compiled/es.json", "intl-compile": "npm run intl-compile-en && npm run intl-compile-es && npm run intl-compile-ca" - }, "eslintConfig": { "extends": "react-app" diff --git a/src/guillo-gmi/actions/add_item.js b/src/guillo-gmi/actions/add_item.tsx similarity index 93% rename from src/guillo-gmi/actions/add_item.js rename to src/guillo-gmi/actions/add_item.tsx index 1da5b259..aeec658b 100644 --- a/src/guillo-gmi/actions/add_item.js +++ b/src/guillo-gmi/actions/add_item.tsx @@ -1,9 +1,12 @@ -import React from 'react' import { useTraversal } from '../contexts' import { Modal } from '../components/modal' import { useCrudContext } from '../hooks/useCrudContext' -export function AddItem(props) { +interface Props { + type: string +} + +export function AddItem(props: Props) { const Ctx = useTraversal() const { post, loading } = useCrudContext() const { type } = props diff --git a/src/guillo-gmi/actions/change_pass.js b/src/guillo-gmi/actions/change_pass.tsx similarity index 80% rename from src/guillo-gmi/actions/change_pass.js rename to src/guillo-gmi/actions/change_pass.tsx index 6a50098a..1dbb0928 100644 --- a/src/guillo-gmi/actions/change_pass.js +++ b/src/guillo-gmi/actions/change_pass.tsx @@ -1,30 +1,28 @@ -import React from 'react' import { useTraversal } from '../contexts' import { Modal } from '../components/modal' import { useCrudContext } from '../hooks/useCrudContext' import { Input } from '../components/input/input' import { Button } from '../components/input/button' import { Form } from '../components/input/form' +import { useState } from 'react' const initial = { pass1: '', pass2: '', } -export function ChangePassword(props) { - const [state, setState] = React.useState(initial) - const [perror, setPerror] = React.useState(undefined) +export function ChangePassword() { + const [state, setState] = useState(initial) + const [perror, setPerror] = useState(undefined) const Ctx = useTraversal() const { patch } = useCrudContext() - // const Form = getForm(type) - const setActive = () => { Ctx.cancelAction() } - async function doSubmit(data) { + async function doSubmit() { if (state.pass1 === '') { setPerror('provide a password') return @@ -37,7 +35,7 @@ export function ChangePassword(props) { setPerror(undefined) - let form = { + const form = { password: state.pass1, } @@ -53,7 +51,7 @@ export function ChangePassword(props) { } const setPass = (field) => (val) => { - let n = {} + const n = {} n[field] = val setState((state) => ({ ...state, ...n })) setPerror(undefined) @@ -61,12 +59,7 @@ export function ChangePassword(props) { return ( -
console.log(err)} - actionName={'Change'} - title={'Change Password'} - > + {perror && (
diff --git a/src/guillo-gmi/actions/copy_item.js b/src/guillo-gmi/actions/copy_item.tsx similarity index 86% rename from src/guillo-gmi/actions/copy_item.js rename to src/guillo-gmi/actions/copy_item.tsx index db94007e..38553e8e 100644 --- a/src/guillo-gmi/actions/copy_item.js +++ b/src/guillo-gmi/actions/copy_item.tsx @@ -1,10 +1,15 @@ -import React from 'react' +import { Fragment } from 'react' import { PathTree } from '../components/modal' import { useTraversal } from '../contexts' import { useCrudContext } from '../hooks/useCrudContext' import { getNewId } from '../lib/utils' +import { ItemModel } from '../models' -export function CopyItem(props) { +interface Props { + item: ItemModel +} + +export function CopyItem(props: Props) { const Ctx = useTraversal() const { post } = useCrudContext() const { item } = props @@ -35,7 +40,7 @@ export function CopyItem(props) { onConfirm={copyItem} onCancel={() => Ctx.cancelAction()} > - + {`New id for "${item['@name']}" copy`} @@ -45,7 +50,7 @@ export function CopyItem(props) { data-test={`inputCopyIdTest-${item['@name']}`} defaultValue={getNewId(item['@name'])} /> - + ) } diff --git a/src/guillo-gmi/actions/copy_items.js b/src/guillo-gmi/actions/copy_items.tsx similarity index 87% rename from src/guillo-gmi/actions/copy_items.js rename to src/guillo-gmi/actions/copy_items.tsx index e983730d..d32a7025 100644 --- a/src/guillo-gmi/actions/copy_items.js +++ b/src/guillo-gmi/actions/copy_items.tsx @@ -1,10 +1,15 @@ -import React from 'react' +import { Fragment } from 'react' import { PathTree } from '../components/modal' import { useTraversal } from '../contexts' import { getNewId } from '../lib/utils' +import { ItemModel } from '../models' const withError = (res) => res.status >= 300 -export function CopyItems(props) { + +interface Props { + items: Array +} +export function CopyItems(props: Props) { const Ctx = useTraversal() const { items = [] } = props @@ -43,7 +48,7 @@ export function CopyItems(props) { onCancel={() => Ctx.cancelAction()} > {items.map((item) => ( - + {`New id for "${item.id}" copy`} @@ -53,7 +58,7 @@ export function CopyItems(props) { data-test={`inputCopyIdTest-${item['@name']}`} defaultValue={getNewId(item.id)} /> - + ))}   diff --git a/src/guillo-gmi/actions/index.js b/src/guillo-gmi/actions/index.ts similarity index 100% rename from src/guillo-gmi/actions/index.js rename to src/guillo-gmi/actions/index.ts diff --git a/src/guillo-gmi/actions/move_item.js b/src/guillo-gmi/actions/move_item.tsx similarity index 89% rename from src/guillo-gmi/actions/move_item.js rename to src/guillo-gmi/actions/move_item.tsx index dba7a55b..c6b02cbb 100644 --- a/src/guillo-gmi/actions/move_item.js +++ b/src/guillo-gmi/actions/move_item.tsx @@ -1,10 +1,14 @@ -import React from 'react' import { PathTree } from '../components/modal' import { useGuillotinaClient, useTraversal } from '../contexts' import { useCrudContext } from '../hooks/useCrudContext' import { useLocation } from '../hooks/useLocation' +import { ItemModel } from '../models' -export function MoveItem(props) { +interface Props { + item: ItemModel +} + +export function MoveItem(props: Props) { const Ctx = useTraversal() const { post } = useCrudContext() const [, navigate] = useLocation() @@ -30,7 +34,6 @@ export function MoveItem(props) { Ctx.flash(`Failed to move item!: ${errorMessage}`, 'danger') } - Ctx.refresh() Ctx.cancelAction() } diff --git a/src/guillo-gmi/actions/move_items.js b/src/guillo-gmi/actions/move_items.tsx similarity index 88% rename from src/guillo-gmi/actions/move_items.js rename to src/guillo-gmi/actions/move_items.tsx index ff66a1bc..f9a28464 100644 --- a/src/guillo-gmi/actions/move_items.js +++ b/src/guillo-gmi/actions/move_items.tsx @@ -1,10 +1,13 @@ -import React from 'react' import { PathTree } from '../components/modal' import { useTraversal } from '../contexts' +import { ItemModel } from '../models' const withError = (res) => res.status >= 300 -export function MoveItems(props) { +interface Props { + items: ItemModel[] +} +export function MoveItems(props: Props) { const Ctx = useTraversal() const { items = [] } = props diff --git a/src/guillo-gmi/actions/remove_item.js b/src/guillo-gmi/actions/remove_item.tsx similarity index 88% rename from src/guillo-gmi/actions/remove_item.js rename to src/guillo-gmi/actions/remove_item.tsx index 333a11c6..c0c86372 100644 --- a/src/guillo-gmi/actions/remove_item.js +++ b/src/guillo-gmi/actions/remove_item.tsx @@ -1,10 +1,14 @@ -import React from 'react' import { Confirm } from '../components/modal' import { useGuillotinaClient, useTraversal } from '../contexts' import { useCrudContext } from '../hooks/useCrudContext' import { useLocation } from '../hooks/useLocation' +import { ItemModel } from '../models' -export function RemoveItem(props) { +interface Props { + item: ItemModel +} + +export function RemoveItem(props: Props) { const Ctx = useTraversal() const { del, loading } = useCrudContext() const [, navigate] = useLocation() @@ -24,7 +28,6 @@ export function RemoveItem(props) { Ctx.flash(`Failed to delete item!: ${errorMessage}`, 'danger') } - Ctx.refresh() Ctx.cancelAction() } diff --git a/src/guillo-gmi/actions/remove_items.js b/src/guillo-gmi/actions/remove_items.tsx similarity index 86% rename from src/guillo-gmi/actions/remove_items.js rename to src/guillo-gmi/actions/remove_items.tsx index e97bdb88..2acf1043 100644 --- a/src/guillo-gmi/actions/remove_items.js +++ b/src/guillo-gmi/actions/remove_items.tsx @@ -1,13 +1,17 @@ -import React from 'react' import { Confirm } from '../components/modal' import { useTraversal } from '../contexts' import { sleep } from '../lib/helpers' import { useConfig } from '../hooks/useConfig' +import { ItemModel } from '../models' +import { useState } from 'react' -export function RemoveItems(props) { +interface Props { + items: ItemModel[] +} +export function RemoveItems(props: Props) { const Ctx = useTraversal() const cfg = useConfig() - const [loading, setLoading] = React.useState(false) + const [loading, setLoading] = useState(false) const { items = [] } = props const last = items[items.length - 1]['@name'] const itemsNames = items @@ -16,7 +20,7 @@ export function RemoveItems(props) { .replace(`, ${last}`, ` and ${last}`) async function removeItems() { - let errors = [] + const errors = [] setLoading(true) const actions = items.map(async (item) => { diff --git a/src/guillo-gmi/components/Link.js b/src/guillo-gmi/components/Link.tsx similarity index 68% rename from src/guillo-gmi/components/Link.js rename to src/guillo-gmi/components/Link.tsx index 3a62f538..57f30e9c 100644 --- a/src/guillo-gmi/components/Link.js +++ b/src/guillo-gmi/components/Link.tsx @@ -1,7 +1,14 @@ -import React from 'react' import { useLocation } from '../hooks/useLocation' +import { ItemModel } from '../models' -export function Link({ aRef, model, children, ...props }) { +interface Props { + aRef?: React.Ref + model: ItemModel + children: React.ReactNode + onClick?: (e: React.MouseEvent) => void +} + +export function Link({ aRef, model, children, ...props }: Props) { const [path, navigate] = useLocation() const aStyle = { textDecoration: 'none', color: 'currentColor' } @@ -15,7 +22,6 @@ export function Link({ aRef, model, children, ...props }) { return ( - - {children} - - - ) -} diff --git a/src/guillo-gmi/components/TdLink.tsx b/src/guillo-gmi/components/TdLink.tsx new file mode 100644 index 00000000..a64336ba --- /dev/null +++ b/src/guillo-gmi/components/TdLink.tsx @@ -0,0 +1,27 @@ +import { useRef } from 'react' +import { Link } from './Link' +import { ItemModel } from '../models' +import { IndexSignature } from '../types/global' + +interface Props { + model: ItemModel + children: React.ReactNode + style?: IndexSignature +} +export function TdLink({ model, children, style = {} }: Props) { + const link = useRef() + + function onClick() { + if (link && link.current) { + link.current.click() + } + } + + return ( + + + {children} + + + ) +} diff --git a/src/guillo-gmi/components/behavior_view.js b/src/guillo-gmi/components/behavior_view.tsx similarity index 75% rename from src/guillo-gmi/components/behavior_view.js rename to src/guillo-gmi/components/behavior_view.tsx index 8a673d49..02c48b0a 100644 --- a/src/guillo-gmi/components/behavior_view.js +++ b/src/guillo-gmi/components/behavior_view.tsx @@ -1,10 +1,14 @@ -import React from 'react' import { useTraversal } from '../contexts' import { get } from '../lib/utils' import { useIntl } from 'react-intl' import { genericMessages } from '../locales/generic_messages' +import { GuillotinaCommonObject, GuillotinaSchema } from '../types/guillotina' -export function BehaviorsView({ context, schema }) { +interface Props { + context: GuillotinaCommonObject + schema: GuillotinaSchema +} +export function BehaviorsView({ context, schema }: Props) { const Ctx = useTraversal() const { getBehavior } = Ctx.registry @@ -23,7 +27,7 @@ export function BehaviorsView({ context, schema }) { } return ( - + <> {behaviors.map((behavior) => (

{behavior}

@@ -31,7 +35,7 @@ export function BehaviorsView({ context, schema }) {
))} -
+ ) } @@ -39,7 +43,7 @@ export function BehaviorNotImplemented() { const intl = useIntl() return ( - {intl.formatMessage(genericMessages.not_implemented)} + {intl.formatMessage(genericMessages.not_implemented)} ) } diff --git a/src/guillo-gmi/components/behaviors/iattachment.js b/src/guillo-gmi/components/behaviors/iattachment.tsx similarity index 77% rename from src/guillo-gmi/components/behaviors/iattachment.js rename to src/guillo-gmi/components/behaviors/iattachment.tsx index e9d361ce..72e326f8 100644 --- a/src/guillo-gmi/components/behaviors/iattachment.js +++ b/src/guillo-gmi/components/behaviors/iattachment.tsx @@ -1,11 +1,23 @@ -import React from 'react' +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { useTraversal } from '../../contexts' import { EditableField } from '../fields/editableField' import { Table } from '../ui' import { useIntl } from 'react-intl' import { genericMessages } from '../../locales/generic_messages' +import { + GuillotinaFile, + GuillotinaSchemaProperties, +} from '../../types/guillotina' + +interface Props { + properties: GuillotinaSchemaProperties + values: { + file: GuillotinaFile + } +} -export function IAttachment({ properties, values }) { +export function IAttachment({ properties, values }: Props) { const intl = useIntl() const Ctx = useTraversal() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') diff --git a/src/guillo-gmi/components/behaviors/idublincore.js b/src/guillo-gmi/components/behaviors/idublincore.tsx similarity index 85% rename from src/guillo-gmi/components/behaviors/idublincore.js rename to src/guillo-gmi/components/behaviors/idublincore.tsx index 7c38380f..79d90c6e 100644 --- a/src/guillo-gmi/components/behaviors/idublincore.js +++ b/src/guillo-gmi/components/behaviors/idublincore.tsx @@ -1,4 +1,4 @@ -import React from 'react' +/* eslint-disable @typescript-eslint/no-explicit-any */ import { useTraversal } from '../../contexts' import { EditableField } from '../fields/editableField' import { Table } from '../ui' @@ -12,7 +12,12 @@ const editableFields = [ 'expiration_date', ] -export function IDublinCore({ properties, values }) { +interface Props { + properties: { [key: string]: any } + values: { [key: string]: any } +} + +export function IDublinCore({ properties, values }: Props) { const intl = useIntl() const Ctx = useTraversal() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') diff --git a/src/guillo-gmi/components/behaviors/iimageattachment.js b/src/guillo-gmi/components/behaviors/iimageattachment.tsx similarity index 92% rename from src/guillo-gmi/components/behaviors/iimageattachment.js rename to src/guillo-gmi/components/behaviors/iimageattachment.tsx index 3e37e854..64e5e497 100644 --- a/src/guillo-gmi/components/behaviors/iimageattachment.js +++ b/src/guillo-gmi/components/behaviors/iimageattachment.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { useTraversal } from '../../contexts' import { EditableField } from '../fields/editableField' import { Table } from '../ui' @@ -6,21 +6,31 @@ import { useConfig } from '../../hooks/useConfig' import { Delete } from '../ui' import { Button } from '../input/button' import { FileUpload } from '../input/upload' -import { Confirm } from '../../components/modal' +import { Confirm } from '../modal' import { useIntl } from 'react-intl' import { genericFileMessages, genericMessages, } from '../../locales/generic_messages' +import { + GuillotinaFile, + GuillotinaSchemaProperties, +} from '../../types/guillotina' const _sizesImages = ['large', 'preview', 'mini', 'thumb'] -export function IImageAttachment({ properties, values }) { +interface Props { + properties: GuillotinaSchemaProperties + values: { + image: GuillotinaFile + } +} +export function IImageAttachment({ properties, values }: Props) { const intl = useIntl() const cfg = useConfig() const Ctx = useTraversal() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') - const sizesImages = cfg.size_images || _sizesImages + const sizesImages = cfg.SizeImages || _sizesImages const [file, setFile] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(undefined) diff --git a/src/guillo-gmi/components/behaviors/imultiattachment.js b/src/guillo-gmi/components/behaviors/imultiattachment.tsx similarity index 88% rename from src/guillo-gmi/components/behaviors/imultiattachment.js rename to src/guillo-gmi/components/behaviors/imultiattachment.tsx index 1c4aee6d..e8c33bee 100644 --- a/src/guillo-gmi/components/behaviors/imultiattachment.js +++ b/src/guillo-gmi/components/behaviors/imultiattachment.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Input } from '../input/input' import { FileUpload } from '../input/upload' import { Button } from '../input/button' @@ -7,18 +6,31 @@ import { useCrudContext } from '../../hooks/useCrudContext' import ErrorZone from '../error_zone' import { EditableField } from '../fields/editableField' import { Delete } from '../ui' -import { Confirm } from '../../components/modal' +import { Confirm } from '../modal' import { Table } from '../ui' import { useIntl } from 'react-intl' import { genericFileMessages, genericMessages, } from '../../locales/generic_messages' +import { LightFile } from '../../types/global' +import { + GuillotinaFile, + GuillotinaSchemaProperties, +} from '../../types/guillotina' -export function IMultiAttachment({ properties, values }) { +interface Props { + properties: GuillotinaSchemaProperties + values: { + files: { + [key: string]: GuillotinaFile + } + } +} +export function IMultiAttachment({ properties, values }: Props) { const intl = useIntl() const [fileKey, setFileKey] = useState('') - const [file, setFile] = useState() + const [file, setFile] = useState(undefined) const [fileKeyToDelete, setFileKeyToDelete] = useState(undefined) const [loading, setLoading] = useState(false) @@ -75,13 +87,11 @@ export function IMultiAttachment({ properties, values }) { setFileKeyToDelete(undefined)} - onConfirm={() => deleteFile(fileKeyToDelete)} - message={ - (intl.formatMessage( - genericFileMessages.confirm_message_delete_file - ), - { fileKeyToDelete }) - } + onConfirm={() => deleteFile()} + message={intl.formatMessage( + genericFileMessages.confirm_message_delete_file, + { fileKeyToDelete } + )} /> )} diff --git a/src/guillo-gmi/components/behaviors/imultiimageattachment.js b/src/guillo-gmi/components/behaviors/imultiimageattachment.tsx similarity index 90% rename from src/guillo-gmi/components/behaviors/imultiimageattachment.js rename to src/guillo-gmi/components/behaviors/imultiimageattachment.tsx index eda2fb13..dff68d92 100644 --- a/src/guillo-gmi/components/behaviors/imultiimageattachment.js +++ b/src/guillo-gmi/components/behaviors/imultiimageattachment.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { Button } from '../input/button' -import { Confirm } from '../../components/modal' +import { Confirm } from '../modal' import { Delete } from '../ui' import { FileUpload } from '../input/upload' import { useCrudContext } from '../../hooks/useCrudContext' @@ -14,10 +14,22 @@ import { genericFileMessages, genericMessages, } from '../../locales/generic_messages' +import { + GuillotinaFile, + GuillotinaSchemaProperties, +} from '../../types/guillotina' const _sizesImages = ['large', 'preview', 'mini', 'thumb'] -export function IMultiImageAttachment({ properties, values }) { +interface Props { + properties: GuillotinaSchemaProperties + values: { + images: { + [key: string]: GuillotinaFile + } + } +} +export function IMultiImageAttachment({ properties, values }: Props) { const intl = useIntl() const cfg = useConfig() const [fileKey, setFileKey] = useState('') @@ -28,7 +40,7 @@ export function IMultiImageAttachment({ properties, values }) { const [error, setError] = useState(undefined) const { Ctx } = useCrudContext() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') - const sizesImages = cfg.size_images || _sizesImages + const sizesImages = cfg.SizeImages || _sizesImages const uploadFile = async (ev) => { ev.preventDefault() @@ -102,12 +114,10 @@ export function IMultiImageAttachment({ properties, values }) { loading={loading} onCancel={() => setFileKeyToDelete(undefined)} onConfirm={() => deleteFile()} - message={ - (intl.formatMessage( - genericFileMessages.confirm_message_delete_file - ), - { fileKeyToDelete }) - } + message={intl.formatMessage( + genericFileMessages.confirm_message_delete_file, + { fileKeyToDelete } + )} /> )} diff --git a/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.js b/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.tsx similarity index 86% rename from src/guillo-gmi/components/behaviors/imultiimageorderedattachment.js rename to src/guillo-gmi/components/behaviors/imultiimageorderedattachment.tsx index d4100c5f..24ebfab3 100644 --- a/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.js +++ b/src/guillo-gmi/components/behaviors/imultiimageorderedattachment.tsx @@ -1,20 +1,40 @@ import { Button } from '../input/button' -import { Confirm } from '../../components/modal' +import { Confirm } from '../modal' import { Delete } from '../ui' import { FileUpload } from '../input/upload' import { useCrudContext } from '../../hooks/useCrudContext' import { EditableField } from '../fields/editableField' import { useConfig } from '../../hooks/useConfig' -import React, { useEffect, useState } from 'react' -import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' +import { useEffect, useState } from 'react' +import { + DragDropContext, + Draggable, + Droppable, + DroppableProvided, + DroppableStateSnapshot, +} from 'react-beautiful-dnd' import { v4 as uuidv4 } from 'uuid' import { defineMessages, useIntl } from 'react-intl' import { genericFileMessages, genericMessages, } from '../../locales/generic_messages' +import { + GuillotinaFile, + GuillotinaSchemaProperties, +} from '../../types/guillotina' -const StrictModeDroppable = ({ children, ...props }) => { +interface StrictModeDroppableProps { + children( + provided: DroppableProvided, + snapshot: DroppableStateSnapshot + ): React.ReactElement + droppableId: string +} +const StrictModeDroppable = ({ + children, + droppableId, +}: StrictModeDroppableProps) => { const [enabled, setEnabled] = useState(false) useEffect(() => { const animation = requestAnimationFrame(() => setEnabled(true)) @@ -26,11 +46,11 @@ const StrictModeDroppable = ({ children, ...props }) => { if (!enabled) { return null } - return {children} + return {children} } const reorder = (list, startIndex, endIndex) => { - const result = Array.from(list) + const result: string[] = Array.from(list) const [removed] = result.splice(startIndex, 1) result.splice(endIndex, 0, removed) @@ -48,10 +68,19 @@ const messages = defineMessages({ defaultMessage: 'Images sorted', }, }) -export function IMultiImageOrderedAttachment({ properties, values }) { + +interface Props { + properties: GuillotinaSchemaProperties + values: { + images: GuillotinaFile[] + } +} +export function IMultiImageOrderedAttachment({ properties, values }: Props) { const intl = useIntl() const cfg = useConfig() - const [sortedList, setSortedList] = useState(Object.keys(values['images'])) + const [sortedList, setSortedList] = useState( + Object.keys(values['images']) + ) const [file, setFile] = useState(null) const [fileKeyToDelete, setFileKeyToDelete] = useState(undefined) @@ -60,7 +89,7 @@ export function IMultiImageOrderedAttachment({ properties, values }) { const { Ctx } = useCrudContext() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') - const sizesImages = cfg.size_images || _sizesImages + const sizesImages = cfg.SizeImages || _sizesImages async function onDragEnd(result) { if (!result.destination) { @@ -152,18 +181,16 @@ export function IMultiImageOrderedAttachment({ properties, values }) { } return ( - + <> {fileKeyToDelete && ( setFileKeyToDelete(undefined)} onConfirm={() => deleteFile()} - message={ - (intl.formatMessage( - genericFileMessages.confirm_message_delete_file - ), - { fileKeyToDelete }) - } + message={intl.formatMessage( + genericFileMessages.confirm_message_delete_file, + { fileKeyToDelete } + )} /> )}
@@ -246,6 +273,6 @@ export function IMultiImageOrderedAttachment({ properties, values }) { {error &&

{error}

}
)} -
+ ) } diff --git a/src/guillo-gmi/components/behaviors/iworkflow.js b/src/guillo-gmi/components/behaviors/iworkflow.tsx similarity index 94% rename from src/guillo-gmi/components/behaviors/iworkflow.js rename to src/guillo-gmi/components/behaviors/iworkflow.tsx index 0762bdaa..db1aa93d 100644 --- a/src/guillo-gmi/components/behaviors/iworkflow.js +++ b/src/guillo-gmi/components/behaviors/iworkflow.tsx @@ -1,9 +1,10 @@ -import React from 'react' import { useTraversal } from '../../contexts' import { Confirm } from '../modal' import { useCrudContext } from '../../hooks/useCrudContext' import { ItemModel } from '../../models' import { defineMessages, useIntl } from 'react-intl' +import { useEffect, useState } from 'react' + import { useVocabulary } from '../../hooks/useVocabulary' import { get } from '../../lib/utils' const messages = defineMessages({ @@ -34,8 +35,8 @@ export function IWorkflow() { const Ctx = useTraversal() const { post, loading } = useCrudContext() const modifyContent = Ctx.hasPerm('guillotina.ModifyContent') - const [definition, setDefinition] = React.useState(undefined) - const [workflowAction, setWorkflowAction] = React.useState(null) + const [definition, setDefinition] = useState(undefined) + const [workflowAction, setWorkflowAction] = useState(null) const model = new ItemModel(Ctx.context) const vocabulary = useVocabulary('workflow_states') const currentState = @@ -49,7 +50,7 @@ export function IWorkflow() { setDefinition(workflow) } - React.useEffect(() => { + useEffect(() => { loadDefinition() }, [Ctx.path]) @@ -104,7 +105,7 @@ export function IWorkflow() { if (definition === undefined) return null return ( - + <> {workflowAction && ( )} - + ) } diff --git a/src/guillo-gmi/components/context_toolbar.js b/src/guillo-gmi/components/context_toolbar.tsx similarity index 87% rename from src/guillo-gmi/components/context_toolbar.js rename to src/guillo-gmi/components/context_toolbar.tsx index 562ef8de..60d115b0 100644 --- a/src/guillo-gmi/components/context_toolbar.js +++ b/src/guillo-gmi/components/context_toolbar.tsx @@ -1,5 +1,4 @@ -import React from 'react' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import Dropdown from './input/dropdown' import useSetState from '../hooks/useSetState' @@ -12,21 +11,25 @@ import { Select } from './input/select' import { useIntl } from 'react-intl' import { genericMessages } from '../locales/generic_messages' -/* eslint jsx-a11y/anchor-is-valid: "off" */ +interface State { + types?: string[] + isActive?: boolean +} const initialState = { types: undefined } export function CreateButton() { const intl = useIntl() - const [state, setState] = useSetState(initialState) + const [state, setState] = useSetState(initialState) const Ctx = useTraversal() const Config = useConfig() useEffect(() => { - ;(async function anyNameFunction() { + async function anyNameFunction() { const types = await Ctx.client.getTypes(Ctx.path) setState({ types: types.filter((item) => !Config.DisabledTypes.includes(item)), }) - })() + } + anyNameFunction() }, [Ctx.path]) const doAction = (item) => { @@ -65,14 +68,17 @@ export function CreateButton() { ) } -export function ContextToolbar({ AddButton, ...props }) { +interface Props { + AddButton?: React.FC +} +export function ContextToolbar({ AddButton }: Props) { const intl = useIntl() - const [state, setState] = useSetState(initialState) + const [state, setState] = useSetState(initialState) const [location, setLocation, del] = useLocation() const traversal = useTraversal() const Config = useConfig() const searchText = location.get('q') - const [searchValue, setSearchValue] = React.useState(searchText || '') + const [searchValue, setSearchValue] = useState(searchText || '') useEffect(() => { loadTypes() @@ -104,7 +110,7 @@ export function ContextToolbar({ AddButton, ...props }) { } return ( - + <>
@@ -144,13 +150,9 @@ export function ContextToolbar({ AddButton, ...props }) {
{traversal.hasPerm('guillotina.AddContent') && (
- {AddButton !== undefined ? ( - - ) : ( - - )} + {AddButton !== undefined ? : }
)} - + ) } diff --git a/src/guillo-gmi/components/error_boundary.js b/src/guillo-gmi/components/error_boundary.tsx similarity index 63% rename from src/guillo-gmi/components/error_boundary.js rename to src/guillo-gmi/components/error_boundary.tsx index dcfffe1f..ebd71d84 100644 --- a/src/guillo-gmi/components/error_boundary.js +++ b/src/guillo-gmi/components/error_boundary.tsx @@ -1,10 +1,17 @@ -import React from 'react' -import { injectIntl } from 'react-intl' +import { Component } from 'react' +import { IntlShape, injectIntl } from 'react-intl' const style = { color: '#F44336', fontSize: 20, paddingBottom: 20 } -class ErrorBoundaryComponent extends React.Component { - state = {} +class ErrorBoundaryComponent extends Component< + { intl: IntlShape; children: React.ReactNode }, + { hasError: boolean; errorMsg: string; errorStack: string } +> { + state = { + hasError: false, + errorMsg: '', + errorStack: '', + } componentDidCatch(error, errorInfo) { this.setState({ @@ -40,4 +47,7 @@ class ErrorBoundaryComponent extends React.Component { } } -export default injectIntl(ErrorBoundaryComponent) +export const ErrorBoundary: React.ComponentType<{ + intl: IntlShape + children: React.ReactNode +}> = injectIntl(ErrorBoundaryComponent) diff --git a/src/guillo-gmi/components/error_zone.js b/src/guillo-gmi/components/error_zone.tsx similarity index 68% rename from src/guillo-gmi/components/error_zone.js rename to src/guillo-gmi/components/error_zone.tsx index 6ca27b0e..722c4bda 100644 --- a/src/guillo-gmi/components/error_zone.js +++ b/src/guillo-gmi/components/error_zone.tsx @@ -1,8 +1,13 @@ -import React from 'react' import PropTypes from 'prop-types' import { classnames } from '../lib/helpers' -const ErrorZone = ({ children, id, className }) => { +interface Props { + children: React.ReactNode + id?: string + className?: string +} + +const ErrorZone = ({ children, id, className = '' }: Props) => { return (

{children} diff --git a/src/guillo-gmi/components/fields/downloadField.js b/src/guillo-gmi/components/fields/downloadField.tsx similarity index 90% rename from src/guillo-gmi/components/fields/downloadField.js rename to src/guillo-gmi/components/fields/downloadField.tsx index faf44f89..b742956c 100644 --- a/src/guillo-gmi/components/fields/downloadField.js +++ b/src/guillo-gmi/components/fields/downloadField.tsx @@ -1,9 +1,17 @@ -import * as React from 'react' import { useTraversal } from '../../contexts' import { useIntl } from 'react-intl' import { genericMessages } from '../../locales/generic_messages' -export const DownloadField = ({ value }) => { +interface DownloadFieldProps { + value: { + data: { + filename: string + content_type: string + } + field: string + } +} +export const DownloadField = ({ value }: DownloadFieldProps) => { const intl = useIntl() const Ctx = useTraversal() const { data, field } = value diff --git a/src/guillo-gmi/components/fields/editComponent.js b/src/guillo-gmi/components/fields/editComponent.tsx similarity index 67% rename from src/guillo-gmi/components/fields/editComponent.js rename to src/guillo-gmi/components/fields/editComponent.tsx index 98108a93..be219cd6 100644 --- a/src/guillo-gmi/components/fields/editComponent.js +++ b/src/guillo-gmi/components/fields/editComponent.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Textarea } from '../input/textarea' import { Checkbox } from '../input/checkbox' import { FileUpload } from '../input/upload' @@ -10,58 +9,74 @@ import { SelectVocabulary } from '../input/select_vocabulary' import { SearchInputList } from '../input/search_input_list' import { SearchInput } from '../input/search_input' import { useTraversal } from '../../contexts' +import { Ref, forwardRef } from 'react' +import { GuillotinaItemsProperty } from '../../types/guillotina' +import { IndexSignature } from '../../types/global' -export const EditComponent = React.forwardRef( - ({ schema, val, setValue, dataTest, className, ...rest }, ref) => { +interface Props { + schema: GuillotinaItemsProperty + val: any + setValue: (value: any) => void + dataTest?: string + className?: string + placeholder?: string + id?: string + required?: boolean +} + +export const EditComponent = forwardRef( + ( + { + schema, + val, + setValue, + dataTest, + className, + placeholder, + id, + required, + }: Props, + ref + ) => { const traversal = useTraversal() if (schema?.widget === 'search_list') { return ( - - {rest.placeholder && ( - - )} + <> + {placeholder && } setValue(ev)} queryCondition={ schema?.queryCondition ? schema.queryCondition : 'title__in' } - dataTest={dataTest} path={schema.queryPath} labelProperty={ schema?.labelProperty ? schema.labelProperty : 'title' } typeNameQuery={schema?.typeNameQuery ? schema.typeNameQuery : null} - {...rest} /> - + ) } else if (schema?.widget === 'search') { return ( - - {rest.placeholder && ( - - )} + <> + {placeholder && } setValue(ev)} queryCondition={ schema?.queryCondition ? schema.queryCondition : 'title__in' } - dataTest={dataTest} path={schema.queryPath} labelProperty={ schema?.labelProperty ? schema.labelProperty : 'title' } typeNameQuery={schema?.typeNameQuery ? schema.typeNameQuery : null} - {...rest} /> - + ) } else if (schema?.widget === 'textarea' || schema?.widget === 'richtext') { return ( @@ -69,20 +84,19 @@ export const EditComponent = React.forwardRef( value={val || ''} className={className} onChange={(ev) => setValue(ev)} - ref={ref} + ref={ref as Ref} dataTest={dataTest} - {...rest} + placeholder={placeholder} + id={id} /> ) } else if (schema?.type === 'boolean') { return ( setValue(ev)} - ref={ref} dataTest={dataTest} - {...rest} /> ) } else if (schema?.type === 'array') { @@ -95,9 +109,10 @@ export const EditComponent = React.forwardRef( className={className} classWrap="is-fullwidth" dataTest={dataTest} - {...rest} onChange={setValue} multiple + placeholder={placeholder} + id={id} /> ) } else if (schema?.items?.vocabulary) { @@ -115,25 +130,23 @@ export const EditComponent = React.forwardRef( })} multiple onChange={setValue} - {...rest} + placeholder={placeholder} + id={id} /> ) } } return ( - - {rest.placeholder && ( - - )} + <> + {placeholder && } setValue(ev)} - ref={ref} + ref={ref as Ref} dataTest={dataTest} - {...rest} /> - + ) } else if (schema?.widget === 'file') { return ( @@ -141,7 +154,6 @@ export const EditComponent = React.forwardRef( onChange={(ev) => setValue(ev)} label={get(val, 'filename', null)} dataTest={dataTest} - {...rest} /> ) } else if (schema?.widget === 'select' && schema.type === 'string') { @@ -155,7 +167,8 @@ export const EditComponent = React.forwardRef( dataTest={dataTest} onChange={setValue} vocabularyName={get(schema, 'vocabularyName', null)} - {...rest} + placeholder={placeholder} + id={id} /> ) } @@ -174,9 +187,35 @@ export const EditComponent = React.forwardRef( } })} onChange={setValue} - {...rest} + placeholder={placeholder} + id={id} /> ) + } else if (schema?.type === 'object' && schema.widget !== 'file') { + const value = val as IndexSignature + return ( + <> + {schema.title &&

{schema.title}

} + {Object.keys(get(schema, 'properties', {})).map((key) => { + const subSchema = get(schema, 'properties', {})[key] + const requiredFields: string[] = get(schema, 'required', []) + return ( + { + setValue({ ...value, [key]: ev }) + }} + dataTest={`${key}TestInput`} + /> + ) + })} + + ) } const getInputType = () => { switch (schema?.type) { @@ -186,6 +225,8 @@ export const EditComponent = React.forwardRef( return 'date' case 'datetime': return 'datetime-local' + case 'time': + return 'time' default: return 'text' } @@ -196,9 +237,11 @@ export const EditComponent = React.forwardRef( className={className} dataTest={dataTest} onChange={(ev) => setValue(ev)} - ref={ref} + ref={ref as Ref} type={getInputType()} - {...rest} + required={required} + placeholder={placeholder} + id={id} /> ) } diff --git a/src/guillo-gmi/components/fields/editableField.js b/src/guillo-gmi/components/fields/editableField.tsx similarity index 96% rename from src/guillo-gmi/components/fields/editableField.js rename to src/guillo-gmi/components/fields/editableField.tsx index 5e63b7bb..c2f50ca7 100644 --- a/src/guillo-gmi/components/fields/editableField.js +++ b/src/guillo-gmi/components/fields/editableField.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Button } from '../input/button' import { useCrudContext } from '../../hooks/useCrudContext' import { useConfig } from '../../hooks/useConfig' @@ -13,6 +12,15 @@ import { genericMessages, } from '../../locales/generic_messages' +interface Props { + field: string + value: any + ns?: string + schema?: any + modifyContent?: boolean + required?: boolean +} + export function EditableField({ field, value, @@ -20,9 +28,9 @@ export function EditableField({ schema = undefined, modifyContent, required, -}) { +}: Props) { const intl = useIntl() - const ref = useRef() + const ref = useRef() const [isEdit, setEdit] = useState(false) const [val, setValue] = useState(value) const { patch, loading, Ctx } = useCrudContext() @@ -134,7 +142,7 @@ export function EditableField({ } return ( - + <> {!isEdit && (
)} -
+ ) } diff --git a/src/guillo-gmi/components/fields/renderField.js b/src/guillo-gmi/components/fields/renderField.tsx similarity index 71% rename from src/guillo-gmi/components/fields/renderField.js rename to src/guillo-gmi/components/fields/renderField.tsx index cda62afa..42f0e55b 100644 --- a/src/guillo-gmi/components/fields/renderField.js +++ b/src/guillo-gmi/components/fields/renderField.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { DownloadField } from './downloadField' import { useIntl } from 'react-intl' import { useVocabulary } from '../../hooks/useVocabulary' @@ -6,10 +6,20 @@ import { get } from '../../lib/utils' import { buildQs } from '../../lib/search' import { useTraversal } from '../../contexts' import { useConfig } from '../../hooks/useConfig' +import { + GuillotinaSchemaProperty, + GuillotinaVocabularyItem, +} from '../../types/guillotina' const plain = ['string', 'number', 'boolean'] -export function RenderField({ value, Widget, schema }) { +interface RenderFieldProps { + value: any + Widget?: React.ComponentType<{ value: any; schema: GuillotinaSchemaProperty }> + schema?: GuillotinaSchemaProperty +} + +export function RenderField({ value, Widget, schema }: RenderFieldProps) { if (value === null || value === undefined) return '' if (Widget) { @@ -29,17 +39,27 @@ export function RenderField({ value, Widget, schema }) { )) } return Object.keys(value).map((key) => ( - + )) } return

No render for {JSON.stringify(value)}

} -const FieldValue = ({ field, value }) => ( +interface FieldValueProps { + field: string + value: unknown + schema: GuillotinaSchemaProperty +} +const FieldValue = ({ field, value, schema }: FieldValueProps) => (
{field}
- +
) @@ -52,16 +72,27 @@ const getDefaultValueEditableField = (intl) => { }) } -export const SearchRenderField = ({ schema, value, modifyContent }) => { +interface SearchRenderFieldProps { + schema: GuillotinaSchemaProperty + value: string | string[] + modifyContent: boolean +} +export const SearchRenderField = ({ + schema, + value, + modifyContent, +}: SearchRenderFieldProps) => { + const intl = useIntl() const [valuesLabels, setValuesLabels] = useState([]) const [isLoadingData, setIsLoadingData] = useState(false) const traversal = useTraversal() const { SearchEngine } = useConfig() + const DEFAULT_VALUE_EDITABLE_FIELD = getDefaultValueEditableField(intl) useEffect(() => { const fetchData = async (valuesToSearch) => { setIsLoadingData(true) - let searchTermQs = [] + let searchTermQs = '' const searchTermParsed = ['__or', `id=${valuesToSearch.join('%26id=')}`] const { get: getSearch } = traversal.registry @@ -121,7 +152,16 @@ export const SearchRenderField = ({ schema, value, modifyContent }) => { return } -export const VocabularyRenderField = ({ schema, value, modifyContent }) => { +interface VocabularyRenderFieldProps { + schema: GuillotinaSchemaProperty + value: string | string[] + modifyContent: boolean +} +export const VocabularyRenderField = ({ + schema, + value, + modifyContent, +}: VocabularyRenderFieldProps) => { const intl = useIntl() const DEFAULT_VALUE_EDITABLE_FIELD = getDefaultValueEditableField(intl) @@ -138,15 +178,18 @@ export const VocabularyRenderField = ({ schema, value, modifyContent }) => { } if (schema?.vocabularyName) { - const vocabularyValue = get(vocabulary, 'data.items', []).find( - (item) => item.token === value - ) + const vocabularyValue = get( + vocabulary, + 'data.items', + [] + ).find((item) => item.token === value) renderProps['value'] = vocabularyValue?.title ?? '' } else { renderProps['value'] = (renderProps['value'] ?? []).map((value) => { return ( - get(vocabulary, 'data.items', []).find((item) => item.token === value) - ?.title ?? '' + get(vocabulary, 'data.items', []).find( + (item) => item.token === value + )?.title ?? '' ) }) } @@ -156,7 +199,18 @@ export const VocabularyRenderField = ({ schema, value, modifyContent }) => { return } -export function RenderFieldComponent({ schema, field, val, modifyContent }) { +interface RenderFieldComponentProps { + schema: GuillotinaSchemaProperty + field: string + val: any + modifyContent?: boolean +} +export function RenderFieldComponent({ + schema, + field, + val, + modifyContent, +}: RenderFieldComponentProps) { const intl = useIntl() const DEFAULT_VALUE_EDITABLE_FIELD = getDefaultValueEditableField(intl) @@ -167,6 +221,7 @@ export function RenderFieldComponent({ schema, field, val, modifyContent }) { (modifyContent ? DEFAULT_VALUE_EDITABLE_FIELD : DEFAULT_VALUE_NO_EDITABLE_FIELD), + schema: schema, } if (val && schema?.widget === 'file') { renderProps['value'] = { @@ -180,14 +235,12 @@ export function RenderFieldComponent({ schema, field, val, modifyContent }) { renderProps['value'] = new Date(val).toLocaleString() } else if (schema?.items?.vocabularyName || schema?.vocabularyName) { renderProps['Widget'] = VocabularyRenderField - renderProps['schema'] = schema } else if ( schema?.widget === 'search' || schema?.widget === 'search_list' ) { renderProps['Widget'] = SearchRenderField renderProps['value'] = val - renderProps['schema'] = schema } return renderProps } diff --git a/src/guillo-gmi/components/flash.js b/src/guillo-gmi/components/flash.tsx similarity index 93% rename from src/guillo-gmi/components/flash.js rename to src/guillo-gmi/components/flash.tsx index 5475f5ec..b58d5a80 100644 --- a/src/guillo-gmi/components/flash.js +++ b/src/guillo-gmi/components/flash.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { useTraversal } from '../contexts' import { Notification } from './ui/notification' import { Delete } from './ui/delete' diff --git a/src/guillo-gmi/components/guillotina.js b/src/guillo-gmi/components/guillotina.tsx similarity index 88% rename from src/guillo-gmi/components/guillotina.js rename to src/guillo-gmi/components/guillotina.tsx index 9b0d59c2..4146109a 100644 --- a/src/guillo-gmi/components/guillotina.js +++ b/src/guillo-gmi/components/guillotina.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react' import { Flash } from './flash' import { TraversalProvider, useGuillotinaClient } from '../contexts' import { useConfig } from '../hooks/useConfig' -import { useRegistry } from '../hooks/useRegistry' +import { IRegistry, useRegistry } from '../hooks/useRegistry' import { useLocation } from '../hooks/useLocation' import { guillotinaReducer } from '../reducers/guillotina' import { initialState } from '../reducers/guillotina' @@ -13,6 +13,8 @@ import { IntlProvider } from 'react-intl' import langCA from '../locales/compiled/ca.json' import langES from '../locales/compiled/es.json' import langEN from '../locales/compiled/en.json' +import { Auth } from '../lib/auth' +import { IndexSignature } from '../types/global' function loadLocaleData(locale) { switch (locale) { @@ -25,7 +27,14 @@ function loadLocaleData(locale) { } } -export function Guillotina({ auth, locale, ...props }) { +interface GuillotinaProps { + auth: Auth + locale: string + url: string + config: IndexSignature + registry: IRegistry +} +export function Guillotina({ auth, locale, ...props }: GuillotinaProps) { const messages = loadLocaleData(locale) const url = props.url || 'http://localhost:8080' // without trailing slash const config = props.config || {} @@ -51,8 +60,8 @@ export function Guillotina({ auth, locale, ...props }) { }, [searchPath]) useEffect(() => { - ;(async () => { - let data = await client.getContext(path) + const initContext = async () => { + const data = await client.getContext(path) if (data.status === 401) { dispatch({ type: 'SET_ERROR', payload: 'notallowed' }) return @@ -60,11 +69,12 @@ export function Guillotina({ auth, locale, ...props }) { dispatch({ type: 'SET_ERROR', payload: 'notfound' }) return } - let context = await data.json() + const context = await data.json() const pr = await client.canido(path, Permissions) const permissions = await pr.json() dispatch({ type: 'SET_CONTEXT', payload: { context, permissions } }) - })() + } + initContext() }, [path, refresh, client]) const ErrorBoundary = registry.get('views', 'ErrorBoundary') diff --git a/src/guillo-gmi/components/index.js b/src/guillo-gmi/components/index.ts similarity index 100% rename from src/guillo-gmi/components/index.js rename to src/guillo-gmi/components/index.ts diff --git a/src/guillo-gmi/components/input/button.js b/src/guillo-gmi/components/input/button.tsx similarity index 68% rename from src/guillo-gmi/components/input/button.js rename to src/guillo-gmi/components/input/button.tsx index bc26423f..666dc3d3 100644 --- a/src/guillo-gmi/components/input/button.js +++ b/src/guillo-gmi/components/input/button.tsx @@ -1,8 +1,14 @@ -import React from 'react' import { classnames } from '../../lib/helpers' -const noop = () => {} - +interface Props { + children: React.ReactNode + className?: string + onClick?: (event: React.MouseEvent) => void + type?: 'submit' | 'reset' | 'button' + loading?: boolean + disabled?: boolean + dataTest?: string +} export const Button = ({ children, className = 'is-primary', @@ -11,11 +17,9 @@ export const Button = ({ loading = false, disabled = false, dataTest, - ...rest -}) => { +}: Props) => { let css = [].concat('button', ...className.split(' ')) if (loading) css = css.concat('is-loading') - if (disabled) onClick = noop return (

@@ -24,7 +28,6 @@ export const Button = ({ className={classnames(css)} onClick={onClick} disabled={disabled} - {...rest} data-test={dataTest} > {children} diff --git a/src/guillo-gmi/components/input/checkbox.js b/src/guillo-gmi/components/input/checkbox.tsx similarity index 62% rename from src/guillo-gmi/components/input/checkbox.js rename to src/guillo-gmi/components/input/checkbox.tsx index 62fb789b..ce3abfb6 100644 --- a/src/guillo-gmi/components/input/checkbox.js +++ b/src/guillo-gmi/components/input/checkbox.tsx @@ -1,6 +1,22 @@ -import React, { useEffect, useRef } from 'react' +import { ChangeEvent, useEffect, useRef, useState } from 'react' import { classnames } from '../../lib/helpers' +interface Props { + className?: string + classNameInput?: string + loading?: boolean + indeterminate?: boolean + backgroundColor?: string + borderColor?: string + dataTest?: string + onChange: (value: boolean) => void + id?: string + disabled?: boolean + checked?: boolean + children?: React.ReactNode + placeholder?: string +} + export const Checkbox = ({ id, className, @@ -8,18 +24,14 @@ export const Checkbox = ({ loading, disabled, indeterminate = false, - value = false, - color, - backgroundColor, - borderColor, + checked, children, placeholder, onChange, dataTest, - ...rest -}) => { +}: Props) => { const inputRef = useRef(null) - const [state, setState] = React.useState(value) + const [state, setState] = useState(checked) useEffect(() => { if (inputRef.current) { @@ -27,7 +39,7 @@ export const Checkbox = ({ } }, [indeterminate]) - const updateState = (ev) => { + const updateState = (ev: ChangeEvent) => { setState(ev.target.checked) onChange(ev.target.checked) } @@ -44,7 +56,6 @@ export const Checkbox = ({ checked={state} onChange={updateState} data-test={dataTest} - {...rest} /> {children || placeholder} diff --git a/src/guillo-gmi/components/input/dropdown.js b/src/guillo-gmi/components/input/dropdown.tsx similarity index 85% rename from src/guillo-gmi/components/input/dropdown.js rename to src/guillo-gmi/components/input/dropdown.tsx index c6e554c2..5cd672d5 100644 --- a/src/guillo-gmi/components/input/dropdown.js +++ b/src/guillo-gmi/components/input/dropdown.tsx @@ -1,6 +1,16 @@ -import React, { useState, useRef } from 'react' +import { useState, useRef } from 'react' import useClickAway from '../../hooks/useClickAway' +interface Props { + children: React.ReactNode + disabled?: boolean + id?: string + isRight?: boolean + onChange: (value: string) => void + optionDisabledWhen?: (option: any) => boolean + options: { text: string; value: string }[] +} + export default function Dropdown({ children, disabled, @@ -9,8 +19,7 @@ export default function Dropdown({ onChange, optionDisabledWhen, options, - ...props -}) { +}: Props) { const ref = useRef(null) const [isActive, setIsActive] = useState(false) const position = isRight ? 'is-right' : '' @@ -23,7 +32,7 @@ export default function Dropdown({ }) return ( -

+
) } - -Form.propTypes = { - children: PropTypes.node.isRequired, - className: PropTypes.string, - onSubmit: PropTypes.func, - onReset: PropTypes.func, - autoComplete: PropTypes.string, -} diff --git a/src/guillo-gmi/components/input/form_builder.js b/src/guillo-gmi/components/input/form_builder.tsx similarity index 64% rename from src/guillo-gmi/components/input/form_builder.js rename to src/guillo-gmi/components/input/form_builder.tsx index f036d5a6..4fcd2abd 100644 --- a/src/guillo-gmi/components/input/form_builder.js +++ b/src/guillo-gmi/components/input/form_builder.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { Form } from './form' import { Input } from './input' import { EmailInput } from './email' @@ -6,6 +5,12 @@ import { PasswordInput } from './password' import { Button } from './button' import { Checkbox } from './checkbox' import { generateUID } from '../../lib/helpers' +import { Children, cloneElement, isValidElement, useRef } from 'react' +import { IndexSignature } from '../../types/global' +import { + GuillotinaSchema, + GuillotinaSchemaProperty, +} from '../../types/guillotina' const formComponents = { string: Input, @@ -14,24 +19,33 @@ const formComponents = { email: EmailInput, } +interface Props { + schema: GuillotinaSchema + formData?: any + onSubmit: (formData: any, initialData: any) => void + actionName: string + children?: React.ReactNode + exclude?: string[] + remotes?: IndexSignature + submitButton?: boolean +} + export function FormBuilder({ schema, formData, - title, onSubmit, actionName, children, exclude = [], - remotes = [], + remotes = {}, submitButton = true, - ...rest -}) { - const ref = React.useRef() +}: Props) { + const ref = useRef() const { properties, required } = schema const values = Object.assign({}, formData || {}) // build initial state - let initialState = {} + const initialState = {} const fields = Object.keys(properties).filter((x) => !exclude.includes(x)) fields.forEach((element) => { @@ -56,17 +70,17 @@ export function FormBuilder({ ref.current[field] = ev.target ? ev.target.value : ev.value || ev } - const GetTag = ({ field }) => { - const Tag = - formComponents[properties[field].widget || properties[field].type] + const GetTag = ({ field }: { field: string }) => { + const property = properties[field] as GuillotinaSchemaProperty + const Tag = formComponents[property.widget || property.type] const props = { - label: properties[field].title, value: initialState[field], onChange: onUpdate(field), - placeholder: properties[field].title || '', + placeholder: property.title || '', id: generateUID(), dataTest: `${field}TestInput`, + required: false, } if (required.includes(field)) { @@ -77,16 +91,20 @@ export function FormBuilder({ return } - const children_ = React.Children.map(children, (child) => - React.cloneElement(child, { onChange: onUpdate }) - ) + const children_ = Children.map(children, (child) => { + if (isValidElement(child)) { + const props = { onChange: onUpdate } + return cloneElement(child, props) + } + return child + }) const changes = () => { onSubmit(ref.current, values) } return ( -
+ {fields.map((field) => ( ))} diff --git a/src/guillo-gmi/components/input/index.js b/src/guillo-gmi/components/input/index.js deleted file mode 100644 index 00373800..00000000 --- a/src/guillo-gmi/components/input/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// @create-index - -export { default as button } from './button.js' -export { default as checkbox } from './checkbox.js' -export { default as email } from './email.js' -export { default as form } from './form.js' -export { default as form_builder } from './form_builder.js' -export { default as input } from './input.js' -export { default as password } from './password.js' -export { default as select } from './select.js' -export { default as select_vocabulary } from './select_vocabulary.js' -export { default as search_input } from './search_input.js' -export { default as input_list } from './input_list.js' diff --git a/src/guillo-gmi/components/input/index.tsx b/src/guillo-gmi/components/input/index.tsx new file mode 100644 index 00000000..18efd288 --- /dev/null +++ b/src/guillo-gmi/components/input/index.tsx @@ -0,0 +1,13 @@ +// @create-index + +export * from './button' +export * from './checkbox' +export * from './email' +export * from './form' +export * from './form_builder' +export * from './input' +export * from './password' +export * from './select' +export * from './select_vocabulary' +export * from './search_input' +export * from './input_list' diff --git a/src/guillo-gmi/components/input/input.js b/src/guillo-gmi/components/input/input.tsx similarity index 64% rename from src/guillo-gmi/components/input/input.js rename to src/guillo-gmi/components/input/input.tsx index 44e67fcc..e67729e7 100644 --- a/src/guillo-gmi/components/input/input.js +++ b/src/guillo-gmi/components/input/input.tsx @@ -1,5 +1,4 @@ -import React, { useState } from 'react' -import PropTypes from 'prop-types' +import { forwardRef, useRef, useState } from 'react' import { classnames, generateUID } from '../../lib/helpers' import ErrorZone from '../error_zone' import useInput from '../../hooks/useInput' @@ -7,9 +6,31 @@ import { notEmpty } from '../../lib/validators' import { useEffect } from 'react' const noop = () => true +interface Props { + name?: string + icon?: JSX.Element + iconPosition?: 'has-icons-left' | 'has-icons-right' + error?: string + errorZoneClassName?: string + autoComplete?: string + className?: string + widget?: string + loading?: boolean + validator?: ((value: string) => boolean) | ((value: string) => boolean)[] + errorMessage?: string + dataTest?: string + autofocus?: boolean + onChange?: (value: string) => void + type?: string + value?: string + required?: boolean + id?: string + placeholder?: string + disabled?: boolean + onKeyUp?: (event: React.KeyboardEvent) => void +} -/** @type any */ -export const Input = React.forwardRef( +export const Input = forwardRef( ( { icon, @@ -20,8 +41,6 @@ export const Input = React.forwardRef( className = '', widget = 'input', type = 'text', - onPressEnter, - isSubmitted, loading = false, required = false, id, @@ -32,28 +51,30 @@ export const Input = React.forwardRef( validator = noop, errorMessage, dataTest = 'testInput', - ...rest + disabled, + onKeyUp, }, ref ) => { + let validatorFn = null if (required) { - validator = Array.isArray(validator) + validatorFn = Array.isArray(validator) ? validator.push(notEmpty) : [validator, notEmpty] } - const { state, ...handlers } = useInput(onChange, value ?? '', validator) + const { state, ...handlers } = useInput(onChange, value ?? '', validatorFn) const [uid] = useState(generateUID('input')) const [mounted, setMounted] = useState(false) // eslint-disable-next-line - ref = ref || React.useRef() + ref = ref || useRef() useEffect(() => { setMounted(true) }, []) useEffect(() => { - if (autofocus && !error) { + if (autofocus && !error && ref != null && typeof ref !== 'function') { ref.current.focus() } }, [mounted, autofocus, ref, error]) @@ -73,7 +94,6 @@ export const Input = React.forwardRef(
{icon && icon}
@@ -97,32 +117,5 @@ export const Input = React.forwardRef( } ) -Input.propTypes = { - icon: PropTypes.node, - iconPosition: PropTypes.arrayOf( - PropTypes.oneOf(['has-icons-left', 'has-icons-right', '']) - ), - error: PropTypes.string, - errorZoneClassName: PropTypes.string, - autoComplete: PropTypes.string, - autoFocus: PropTypes.bool, - className: PropTypes.string, - disabled: PropTypes.bool, - loading: PropTypes.bool, - isSubmitted: PropTypes.bool, - id: PropTypes.string, - name: PropTypes.string, - onChange: PropTypes.func, - onKeyDown: PropTypes.func, - onKeyUp: PropTypes.func, - onPressEnter: PropTypes.func, - placeholder: PropTypes.string, - readOnly: PropTypes.bool, - required: PropTypes.bool, - type: PropTypes.string, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.bool, - ]), -} +Input.displayName = 'Input' +export default Input diff --git a/src/guillo-gmi/components/input/input_list.js b/src/guillo-gmi/components/input/input_list.js deleted file mode 100644 index f8e8dcf2..00000000 --- a/src/guillo-gmi/components/input/input_list.js +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react' -import { Input } from './input' -import { useIntl } from 'react-intl' - -export const InputList = React.forwardRef( - ({ value, onChange, dataTest }, ref) => { - const intl = useIntl() - const [inputValue, setInputValue] = React.useState('') - const addTags = (event) => { - if (event.key === 'Enter' && event.target.value !== '') { - onChange([...value, event.target.value]) - setInputValue('') - } - } - - return ( -
- {(value ?? []).length > 0 && ( -
- {value.map((tag, index) => ( -
- {tag} - -
- ))} -
- )} - addTags(event)} - value={inputValue} - ref={ref} - dataTest={dataTest} - onChange={(value) => { - if (value !== '') { - setInputValue(value) - } - }} - /> -
- ) - } -) diff --git a/src/guillo-gmi/components/input/input_list.tsx b/src/guillo-gmi/components/input/input_list.tsx new file mode 100644 index 00000000..f20d68f5 --- /dev/null +++ b/src/guillo-gmi/components/input/input_list.tsx @@ -0,0 +1,68 @@ +import { InputHTMLAttributes, forwardRef, useState } from 'react' +import { Input } from './input' +import { useIntl } from 'react-intl' + +interface Props { + value: string[] + onChange: (value: string[]) => void + dataTest?: string + id?: string +} +export const InputList = forwardRef< + HTMLInputElement, + InputHTMLAttributes & Props +>(({ value, onChange, dataTest, id }, ref) => { + const intl = useIntl() + const [inputValue, setInputValue] = useState('') + const addTags = (event) => { + if (event.key === 'Enter' && event.target.value !== '') { + onChange([...value, event.target.value]) + setInputValue('') + } + } + + return ( +
+ {(value ?? []).length > 0 && ( +
+ {value.map((tag, index) => ( +
+ {tag} +
+ ))} +
+ )} + + addTags(event)} + value={inputValue} + ref={ref} + dataTest={dataTest} + onChange={(value) => { + setInputValue(value) + }} + /> +
+ ) +}) + +InputList.displayName = 'InputList' +export default InputList diff --git a/src/guillo-gmi/components/input/password.js b/src/guillo-gmi/components/input/password.js deleted file mode 100644 index 9214a5b7..00000000 --- a/src/guillo-gmi/components/input/password.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import { Input } from './input' - -export const PasswordInput = ({ value = '', ...rest }) => { - return -} diff --git a/src/guillo-gmi/components/input/password.tsx b/src/guillo-gmi/components/input/password.tsx new file mode 100644 index 00000000..80517d31 --- /dev/null +++ b/src/guillo-gmi/components/input/password.tsx @@ -0,0 +1,23 @@ +import { InputHTMLAttributes } from 'react' +import { Input } from './input' + +interface Props { + value: string + dataTest: string + onChange: (value: string) => void +} + +export const PasswordInput = ({ + value, + dataTest, + onChange, +}: Props & InputHTMLAttributes) => { + return ( + + ) +} diff --git a/src/guillo-gmi/components/input/search_input.js b/src/guillo-gmi/components/input/search_input.tsx similarity index 83% rename from src/guillo-gmi/components/input/search_input.js rename to src/guillo-gmi/components/input/search_input.tsx index 8528b603..7d01d2d9 100644 --- a/src/guillo-gmi/components/input/search_input.js +++ b/src/guillo-gmi/components/input/search_input.tsx @@ -1,6 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react' - -import PropTypes from 'prop-types' +import { useState, useEffect, useCallback, useRef } from 'react' import { buildQs } from '../../lib/search' import { parser } from '../../lib/search' import useSetState from '../../hooks/useSetState' @@ -12,10 +10,15 @@ import { useIntl } from 'react-intl' import { genericMessages } from '../../locales/generic_messages' import useClickAway from '../../hooks/useClickAway' import { get } from '../../lib/utils' +import { SearchItem } from '../../types/guillotina' +import { Traversal } from '../../contexts' + function debounce(func, wait) { let timeout return function () { + // eslint-disable-next-line @typescript-eslint/no-this-alias const context = this + // eslint-disable-next-line prefer-rest-params const args = arguments const later = function () { timeout = null @@ -26,13 +29,37 @@ function debounce(func, wait) { } } -const initialState = { +interface State { + page: number + items: SearchItem[] + loading: boolean + items_total: number +} +const initialState: State = { page: 0, items: undefined, loading: false, items_total: 0, } +interface Props { + onChange?: (value: string) => void + error?: string + errorZoneClassName?: string + traversal?: Traversal + path?: string + qs?: string[][] + queryCondition?: string + value?: string + btnClass?: string + dataTestWrapper?: string + dataTestSearchInput?: string + dataTestItem?: string + renderTextItemOption?: (item: SearchItem) => string + typeNameQuery?: string + labelProperty?: string +} + export const SearchInput = ({ onChange, error, @@ -49,15 +76,15 @@ export const SearchInput = ({ renderTextItemOption = null, typeNameQuery = null, labelProperty = 'id', -}) => { +}: Props) => { const intl = useIntl() - const [options, setOptions] = useSetState(initialState) - const [isOpen, setIsOpen] = React.useState(false) - const [searchTerm, setSearchTerm] = React.useState('') - const inputRef = React.useRef(null) - const wrapperRef = React.useRef(null) + const [options, setOptions] = useSetState(initialState) + const [isOpen, setIsOpen] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + const inputRef = useRef(null) + const wrapperRef = useRef(null) const { PageSize, SearchEngine } = useConfig() - const [valueLabel, setValueLabel] = useState(undefined) + const [valueLabel, setValueLabel] = useState>(undefined) const [uid] = useState(generateUID('search_input')) useClickAway(wrapperRef, () => { @@ -77,14 +104,14 @@ export const SearchInput = ({ return { maxHeight: 'auto' } } - const delayedQuery = useCallback( + const delayedQuery = useCallback<(value: string) => void>( debounce((value) => handleSearch(0, false, value), 500), [] ) const inicializeLabels = async () => { if (labelProperty !== 'id' && value) { - let searchTermQs = [] + let searchTermQs = '' const searchTermParsed = [`id`, value] const { get: getSearch } = traversal.registry const fnName = getSearch('searchEngineQueryParamsFunction', SearchEngine) @@ -127,20 +154,20 @@ export const SearchInput = ({ const handleSearch = async (page = 0, concat = false, value = '') => { setOptions({ loading: true }) - let searchTermQs = [] + let searchTermQs = '' let searchTermParsed = [] if (value !== '') { searchTermParsed = parser(`${queryCondition}=${value}`) } const { get } = traversal.registry const fnName = get('searchEngineQueryParamsFunction', SearchEngine) - let qsParsed = traversal.client[fnName]({ + const qsParsed = traversal.client[fnName]({ path: traversal.path, start: page * PageSize, pageSize: PageSize, withDepth: false, }) - let sortParsed = parser(`_sort_des=${labelProperty}`) + const sortParsed = parser(`_sort_des=${labelProperty}`) let typeNameParsed = [] if (typeNameQuery) { typeNameParsed = parser(`type_name__in=${typeNameQuery}`) @@ -198,7 +225,7 @@ export const SearchInput = ({ } return ( - + <>
{ - ev.target.blur() + ev.preventDefault() + if (ev.target instanceof HTMLElement) { + ev.target.blur() + } + setIsOpen(!isOpen) if (!options.loading && !options.items) { handleSearch(options.page) @@ -268,6 +299,7 @@ export const SearchInput = ({ }`} data-test={`${dataTestItem}-${item.id}`} onMouseDown={(ev) => { + ev.stopPropagation() ev.preventDefault() if (onChange) { onChange(item.id) @@ -288,7 +320,7 @@ export const SearchInput = ({ )} {options.items && options.items_total > options.items.length && ( - + <>
{intl.formatMessage(genericMessages.load_more)}
-
+ )}
@@ -310,24 +342,6 @@ export const SearchInput = ({ {error ? error : ''} )} - + ) } - -SearchInput.propTypes = { - onChange: PropTypes.func, - value: PropTypes.string, - path: PropTypes.string, - btnClass: PropTypes.string, - error: PropTypes.string, - errorZoneClassName: PropTypes.string, - traversal: PropTypes.object, - qs: PropTypes.array, - queryCondition: PropTypes.string, - dataTestWrapper: PropTypes.string, - dataTestSearchInput: PropTypes.string, - dataTestItem: PropTypes.string, - renderTextItemOption: PropTypes.func, - typeNameQuery: PropTypes.string, - labelProperty: PropTypes.string, -} diff --git a/src/guillo-gmi/components/input/search_input_list.js b/src/guillo-gmi/components/input/search_input_list.tsx similarity index 85% rename from src/guillo-gmi/components/input/search_input_list.js rename to src/guillo-gmi/components/input/search_input_list.tsx index 55de4c1d..0175c296 100644 --- a/src/guillo-gmi/components/input/search_input_list.js +++ b/src/guillo-gmi/components/input/search_input_list.tsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect, useCallback } from 'react' +import { useState, useCallback, useRef, useEffect } from 'react' -import PropTypes from 'prop-types' import { buildQs } from '../../lib/search' import { parser } from '../../lib/search' import useSetState from '../../hooks/useSetState' @@ -12,10 +11,15 @@ import { useIntl } from 'react-intl' import { genericMessages } from '../../locales/generic_messages' import useClickAway from '../../hooks/useClickAway' import { get } from '../../lib/utils' +import { SearchItem } from '../../types/guillotina' +import { Traversal } from '../../contexts' + function debounce(func, wait) { let timeout return function () { + // eslint-disable-next-line @typescript-eslint/no-this-alias const context = this + // eslint-disable-next-line prefer-rest-params const args = arguments const later = function () { timeout = null @@ -26,13 +30,37 @@ function debounce(func, wait) { } } -const initialState = { +interface State { + page: number + items: SearchItem[] + loading: boolean + items_total: number +} +const initialState: State = { page: 0, items: undefined, loading: false, items_total: 0, } +interface Props { + onChange: (value: string[]) => void + error?: string + errorZoneClassName?: string + traversal?: Traversal + path?: string + qs?: string[] + queryCondition?: string + value: string[] + btnClass?: string + dataTestWrapper?: string + dataTestSearchInput?: string + dataTestItem?: string + renderTextItemOption?: (item: SearchItem) => string + typeNameQuery?: string + labelProperty?: string +} + export const SearchInputList = ({ onChange, error, @@ -49,14 +77,14 @@ export const SearchInputList = ({ renderTextItemOption = null, typeNameQuery = null, labelProperty = 'id', -}) => { +}: Props) => { const intl = useIntl() - const [options, setOptions] = useSetState(initialState) + const [options, setOptions] = useSetState(initialState) const [valuesLabel, setValuesLabels] = useState(undefined) - const [isOpen, setIsOpen] = React.useState(false) - const [searchTerm, setSearchTerm] = React.useState('') - const inputRef = React.useRef(null) - const wrapperRef = React.useRef(null) + const [isOpen, setIsOpen] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + const inputRef = useRef(null) + const wrapperRef = useRef(null) const { PageSize, SearchEngine } = useConfig() const [isLoadingData, setIsLoadingData] = useState(false) @@ -79,27 +107,27 @@ export const SearchInputList = ({ return { maxHeight: 'auto' } } - const delayedQuery = useCallback( + const delayedQuery = useCallback<(value: string) => void>( debounce((value) => handleSearch(0, false, value), 500), [] ) const handleSearch = async (page = 0, concat = false, value = '') => { setOptions({ loading: true }) - let searchTermQs = [] + let searchTermQs = '' let searchTermParsed = [] if (value !== '') { searchTermParsed = parser(`${queryCondition}=${value}`) } const { get } = traversal.registry const fnName = get('searchEngineQueryParamsFunction', SearchEngine) - let qsParsed = traversal.client[fnName]({ + const qsParsed = traversal.client[fnName]({ path: traversal.path, start: page * PageSize, pageSize: PageSize, withDepth: false, }) - let sortParsed = parser(`_sort_des=${labelProperty}`) + const sortParsed = parser(`_sort_des=${labelProperty}`) let typeNameParsed = [] if (typeNameQuery) { typeNameParsed = parser(`type_name__in=${typeNameQuery}`) @@ -140,7 +168,7 @@ export const SearchInputList = ({ const inicializeLabels = async () => { if (labelProperty !== 'id' && value.length > 0) { setIsLoadingData(true) - let searchTermQs = [] + let searchTermQs = '' const searchTermParsed = ['__or', `id=${value.join('%26id=')}`] const { get: getSearch } = traversal.registry const fnName = getSearch('searchEngineQueryParamsFunction', SearchEngine) @@ -184,14 +212,16 @@ export const SearchInputList = ({ } } - const renderTextItemOptionFn = (item) => { + const renderTextItemOptionFn = ( + item: SearchItem + ): string | React.ReactNode => { if (renderTextItemOption) { return renderTextItemOption(item) } - return get(item, labelProperty, item.title) || item['@name'] + return get(item, labelProperty, item.title) || item['@name'] } - React.useEffect(() => { + useEffect(() => { if (!options.loading && !options.items && value.length > 0) { inicializeLabels() } else if (value.length === 0) { @@ -204,7 +234,7 @@ export const SearchInputList = ({ } return ( - + <>
{value.map((tag, index) => (
{ @@ -315,7 +345,7 @@ export const SearchInputList = ({ )} {options.items && options.items_total > options.items.length && ( - + <>
{intl.formatMessage(genericMessages.load_more)}
-
+ )}
@@ -337,24 +367,6 @@ export const SearchInputList = ({ {error ? error : ''} )} -
+ ) } - -SearchInputList.propTypes = { - onChange: PropTypes.func, - path: PropTypes.string, - btnClass: PropTypes.string, - dataTestWrapper: PropTypes.string, - dataTestSearchInput: PropTypes.string, - dataTestItem: PropTypes.string, - renderTextItemOption: PropTypes.func, - typeNameQuery: PropTypes.string, - labelProperty: PropTypes.string, - error: PropTypes.string, - errorZoneClassName: PropTypes.string, - traversal: PropTypes.object, - path: PropTypes.string, - qs: PropTypes.array, - queryCondition: PropTypes.string, -} diff --git a/src/guillo-gmi/components/input/select.js b/src/guillo-gmi/components/input/select.tsx similarity index 75% rename from src/guillo-gmi/components/input/select.js rename to src/guillo-gmi/components/input/select.tsx index 07a28909..c585d00b 100644 --- a/src/guillo-gmi/components/input/select.js +++ b/src/guillo-gmi/components/input/select.tsx @@ -1,14 +1,31 @@ -import React, { useState } from 'react' -import PropTypes from 'prop-types' import ErrorZone from '../error_zone' import { classnames, generateUID } from '../../lib/helpers' -import { get } from '../../lib/utils' import { useIntl } from 'react-intl' import { genericMessages } from '../../locales/generic_messages' +import { forwardRef, useState } from 'react' +import { IndexSignature } from '../../types/global' // @ TODO implement hasErrors -/** @type any */ -export const Select = React.forwardRef( +interface Props { + error?: string + errorZoneClassName?: string + size?: number + placeholder?: string + id?: string + className?: string + classWrap?: string + disabled?: boolean + multiple?: boolean + loading?: boolean + onChange?: (value: string | string[]) => void + options: { text: string; value: string }[] + appendDefault?: boolean + style?: IndexSignature + dataTest?: string + value?: string | string[] +} + +export const Select = forwardRef( ( { options, @@ -21,13 +38,12 @@ export const Select = React.forwardRef( classWrap = '', multiple = false, loading = false, - isSubmitted, onChange, appendDefault = false, style = {}, dataTest, value, - ...rest + disabled, }, ref ) => { @@ -42,8 +58,7 @@ export const Select = React.forwardRef( } onChange(selectValue) } else { - const selectValue = get(ev, 'target.value', undefined) - onChange(selectValue) + onChange(ev.target.value) } } @@ -73,13 +88,12 @@ export const Select = React.forwardRef( className={classnames(['', className])} size={multiple ? 5 : size} multiple={multiple} - disabled={loading || rest.disabled} + disabled={loading || disabled} onChange={onUpdate} ref={ref} style={style} data-test={dataTest} value={value} - {...rest} > {options.map(({ text, ...rest }, index) => (
@@ -53,3 +63,6 @@ export const Textarea = React.forwardRef( ) } ) + +Textarea.displayName = 'Textarea' +export default Textarea diff --git a/src/guillo-gmi/components/input/upload.js b/src/guillo-gmi/components/input/upload.tsx similarity index 54% rename from src/guillo-gmi/components/input/upload.js rename to src/guillo-gmi/components/input/upload.tsx index 8269656d..9ccd4be3 100644 --- a/src/guillo-gmi/components/input/upload.js +++ b/src/guillo-gmi/components/input/upload.tsx @@ -1,12 +1,22 @@ -import React from 'react' -import { lightFileReader } from '../../lib/client' +import { ChangeEvent, InputHTMLAttributes } from 'react' +import { lightFileReader } from '../../lib/client.js' import { useIntl } from 'react-intl' +import { LightFile } from '../../types/global' -export function FileUpload({ label, onChange, ...props }) { +interface Props { + label?: string + dataTest?: string + onChange: (file: LightFile) => void +} +export function FileUpload({ + label, + onChange, + dataTest, +}: Props & InputHTMLAttributes) { const intl = useIntl() - const changed = async (event) => { - const file = await lightFileReader(event.target.files[0]) - onChange(file) + const changed = async (event: ChangeEvent) => { + const fileToUpload = await lightFileReader(event.target.files[0]) + onChange(fileToUpload) } return ( @@ -14,10 +24,10 @@ export function FileUpload({ label, onChange, ...props }) {