From e34b0f1f577fcd2f1b383359bb18fa6a8fe08a32 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 8 Feb 2024 11:46:50 +0000 Subject: [PATCH 001/150] Merge pull request #3110 from LiteFarmOrg/LF-3946-show-a-fallback-screen-instead-of-white-screen LF-3946 Show a fallback screen instead of white screen --- packages/webapp/package.json | 1 + packages/webapp/pnpm-lock.yaml | 77 +++++---- packages/webapp/public/locales/en/common.json | 1 + .../webapp/public/locales/en/translation.json | 8 + packages/webapp/public/locales/es/common.json | 1 + .../webapp/public/locales/es/translation.json | 8 + packages/webapp/public/locales/fr/common.json | 1 + .../webapp/public/locales/fr/translation.json | 12 +- packages/webapp/public/locales/pt/common.json | 1 + .../webapp/public/locales/pt/translation.json | 8 + packages/webapp/src/assets/colors.scss | 4 + .../images/errorFallback/background.svg | 24 +++ .../errorFallback/background_mobile.svg | 9 ++ .../images/errorFallback/farmer_desktop.svg | 10 ++ .../images/errorFallback/farmer_mobile.svg | 19 +++ .../assets/images/errorFallback/logout.svg | 5 + .../assets/images/errorFallback/refresh.svg | 10 ++ .../PureReactErrorFallback/index.tsx | 85 ++++++++++ .../PureReactErrorFallback/styles.module.scss | 149 ++++++++++++++++++ .../Finances/AddTransactionButton/index.jsx | 6 +- .../ErrorHandler/ReactErrorFallback/index.tsx | 50 ++++++ .../webapp/src/hooks/useIsAboveBreakpoint.js | 38 ----- packages/webapp/src/main.jsx | 14 +- .../ReactErrorFallback.stories.tsx | 62 ++++++++ .../ReactErrorFallback/styles.module.scss | 24 +++ packages/webapp/src/util/constants.js | 1 + 26 files changed, 548 insertions(+), 80 deletions(-) create mode 100644 packages/webapp/src/assets/images/errorFallback/background.svg create mode 100644 packages/webapp/src/assets/images/errorFallback/background_mobile.svg create mode 100644 packages/webapp/src/assets/images/errorFallback/farmer_desktop.svg create mode 100644 packages/webapp/src/assets/images/errorFallback/farmer_mobile.svg create mode 100644 packages/webapp/src/assets/images/errorFallback/logout.svg create mode 100644 packages/webapp/src/assets/images/errorFallback/refresh.svg create mode 100644 packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/index.tsx create mode 100644 packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/styles.module.scss create mode 100644 packages/webapp/src/containers/ErrorHandler/ReactErrorFallback/index.tsx delete mode 100644 packages/webapp/src/hooks/useIsAboveBreakpoint.js create mode 100644 packages/webapp/src/stories/ReactErrorFallback/ReactErrorFallback.stories.tsx create mode 100644 packages/webapp/src/stories/ReactErrorFallback/styles.module.scss diff --git a/packages/webapp/package.json b/packages/webapp/package.json index b74e61d87e..6a78c2eb13 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -54,6 +54,7 @@ "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-google-login": "^5.2.2", "react-hook-form": "^7.40.0", "react-i18next": "^11.18.6", diff --git a/packages/webapp/pnpm-lock.yaml b/packages/webapp/pnpm-lock.yaml index 12bd03d1ee..b60223af70 100644 --- a/packages/webapp/pnpm-lock.yaml +++ b/packages/webapp/pnpm-lock.yaml @@ -121,6 +121,9 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-error-boundary: + specifier: ^4.0.12 + version: 4.0.12(react@18.2.0) react-google-login: specifier: ^5.2.2 version: 5.2.2(react-dom@18.2.0)(react@18.2.0) @@ -7446,11 +7449,11 @@ packages: resolution: {integrity: sha512-YppvPa1qMyC+oCQJ3tf7Quzpf2NnBlvIRLPJiGAMssUwX5qE0iKe9lTtkNwMaNxEvzz6rDxewSlz+f/MWr4gPw==} dev: true - /@storybook/channels@8.0.0-alpha.0: - resolution: {integrity: sha512-QMDocSVZwyG8EnN4j6N8atejFPbfHTqge+fNDVWUVN1UpNOxAIMOrwrNWcieWB6IpM70k2+HYjcn1cGoAbWT2g==} + /@storybook/channels@8.0.0-alpha.17: + resolution: {integrity: sha512-TZKHO8K6d+Y7UDMQr1P2lqOeZ6TtkxDrcbDHauk47Bh/b4BtIJ78PBBZVDt198zw0kL3CAQ1CVNvdTaSIDOBXw==} dependencies: - '@storybook/client-logger': 8.0.0-alpha.0 - '@storybook/core-events': 8.0.0-alpha.0 + '@storybook/client-logger': 8.0.0-alpha.17 + '@storybook/core-events': 8.0.0-alpha.17 '@storybook/global': 5.0.0 qs: 6.11.2 telejson: 7.2.0 @@ -7519,8 +7522,8 @@ packages: '@storybook/global': 5.0.0 dev: true - /@storybook/client-logger@8.0.0-alpha.0: - resolution: {integrity: sha512-ppQal8eH1YVOiEf9Wg8hKksAf2pF++uSOJcRygkX3KZNCtW6YsSQOZbYsHtNmgzq0wi0ugAg9K8XC0WZUfz2vA==} + /@storybook/client-logger@8.0.0-alpha.17: + resolution: {integrity: sha512-qsMTZD9HA34Jv6HezF6MhO8McYnQUOiEfVoUquVJVeuVcnnuQ5Fi8XXdxhcEMAAFqpWPL24twGFuDY2zkXyCvQ==} dependencies: '@storybook/global': 5.0.0 dev: true @@ -7680,8 +7683,8 @@ packages: resolution: {integrity: sha512-sNnqgO5i5DUIqeQfNbr987KWvAciMN9FmMBuYdKjVFMqWFyr44HTgnhfKwZZKl+VMDYkHA9Do7UGSYZIKy0P4g==} dev: true - /@storybook/core-events@8.0.0-alpha.0: - resolution: {integrity: sha512-9LEyuEL9Bufni7T5FGSlz1tVJu+zyJZmnAF8YCD8QdG61F+HEPiWHypzOUVvLC1+rJRZ5nfe7GVkRr9/FS+TSQ==} + /@storybook/core-events@8.0.0-alpha.17: + resolution: {integrity: sha512-yG8fzR8y8+3ZPBMGWgiyOM8z0Yjp0VDgr42xKe+6lg+ssFZRIrWKanrsb/IUkkqbiwEitfod43BiZiqqNkIMlA==} dependencies: ts-dedent: 2.2.0 dev: true @@ -7851,14 +7854,14 @@ packages: '@storybook/preview-api': 7.0.27 dev: true - /@storybook/instrumenter@8.0.0-alpha.0: - resolution: {integrity: sha512-3tUFmjtR9eZxCm1k1QhNr4bAv1OhGA4FXA+H07doGbLgvITOkzi31Mq1C6jMln/4F7fFEGHNnhWkeojxP57TkQ==} + /@storybook/instrumenter@8.0.0-alpha.17: + resolution: {integrity: sha512-PXbi59y0QjAOgitp0vyhOXm6InG7iEV+thkLisKnWPkmV6VSyw9gRehAbAW0LnfEcj8JWaXRvJEHKxHB5+C2HQ==} dependencies: - '@storybook/channels': 8.0.0-alpha.0 - '@storybook/client-logger': 8.0.0-alpha.0 - '@storybook/core-events': 8.0.0-alpha.0 + '@storybook/channels': 8.0.0-alpha.17 + '@storybook/client-logger': 8.0.0-alpha.17 + '@storybook/core-events': 8.0.0-alpha.17 '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.0-alpha.0 + '@storybook/preview-api': 8.0.0-alpha.17 '@vitest/utils': 0.34.6 util: 0.12.5 dev: true @@ -7971,21 +7974,21 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview-api@8.0.0-alpha.0: - resolution: {integrity: sha512-4Yjh7Eu/5NhBN50ysEEIHuyR/FrQY8g1xFejLMlKdhoVGFRUtuI+qi1KxgxtXfiSxY58xPM8vdKq6EtyKamPrA==} + /@storybook/preview-api@8.0.0-alpha.17: + resolution: {integrity: sha512-F7xFSJr2K8sXLdFE9HJzS4T9YyPXFxCk3NbTu8EljXZvZfNpphCQRz/uEPdZzOp3Cuqn/0+vh6j9hvQ/m/OB3A==} dependencies: - '@storybook/channels': 8.0.0-alpha.0 - '@storybook/client-logger': 8.0.0-alpha.0 - '@storybook/core-events': 8.0.0-alpha.0 + '@storybook/channels': 8.0.0-alpha.17 + '@storybook/client-logger': 8.0.0-alpha.17 + '@storybook/core-events': 8.0.0-alpha.17 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/types': 8.0.0-alpha.0 + '@storybook/types': 8.0.0-alpha.17 '@types/qs': 6.9.7 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 qs: 6.11.2 - synchronous-promise: 2.0.17 + tiny-invariant: 1.3.1 ts-dedent: 2.2.0 util-deprecate: 1.0.2 dev: true @@ -8204,8 +8207,8 @@ packages: /@storybook/testing-library@0.0.14-next.1: resolution: {integrity: sha512-1CAl40IKIhcPaCC4pYCG0b9IiYNymktfV/jTrX7ctquRY3akaN7f4A1SippVHosksft0M+rQTFE0ccfWW581fw==} dependencies: - '@storybook/client-logger': 8.0.0-alpha.0 - '@storybook/instrumenter': 8.0.0-alpha.0 + '@storybook/client-logger': 8.0.0-alpha.17 + '@storybook/instrumenter': 8.0.0-alpha.17 '@testing-library/dom': 8.20.1 '@testing-library/user-event': 13.5.0(@testing-library/dom@8.20.1) ts-dedent: 2.2.0 @@ -8248,11 +8251,10 @@ packages: file-system-cache: 2.3.0 dev: true - /@storybook/types@8.0.0-alpha.0: - resolution: {integrity: sha512-BEowwnvOINs27DRorIoKzKzXMqcgG1m0O6/v5XL/pHT8F9rB+GkD2jYEPlPg5+AdtVZT6aDbOzBlRpAkq/bu9Q==} + /@storybook/types@8.0.0-alpha.17: + resolution: {integrity: sha512-aKldT3ZJ2a1rJVML8s6WHS4xXjvH1krxn3vwMUaigzAI3B5V+BRrZyXAaqgTKPqsKLprliCI1M04gL6H90MJ+g==} dependencies: - '@storybook/channels': 8.0.0-alpha.0 - '@types/babel__core': 7.20.3 + '@storybook/channels': 8.0.0-alpha.17 '@types/express': 4.17.17 file-system-cache: 2.3.0 dev: true @@ -8403,7 +8405,7 @@ packages: engines: {node: '>=12'} dependencies: '@babel/code-frame': 7.22.5 - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.23.5 '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 @@ -8417,7 +8419,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.22.5 - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.23.5 '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 @@ -8445,7 +8447,7 @@ packages: optional: true dependencies: '@adobe/css-tools': 4.3.2 - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.23.5 '@types/jest': 28.1.3 aria-query: 5.3.0 chalk: 3.0.0 @@ -8463,7 +8465,7 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.23.5 '@testing-library/dom': 8.20.1 dev: true @@ -10116,7 +10118,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.23.5 cosmiconfig: 7.1.0 resolve: 1.22.2 @@ -17902,7 +17904,7 @@ packages: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} engines: {node: '>=10'} dependencies: - '@babel/runtime': 7.22.5 + '@babel/runtime': 7.23.5 dev: true /popmotion@11.0.3: @@ -18320,6 +18322,15 @@ packages: react-is: 18.1.0 dev: true + /react-error-boundary@4.0.12(react@18.2.0): + resolution: {integrity: sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.23.5 + react: 18.2.0 + dev: false + /react-google-login@5.2.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JUngfvaSMcOuV0lFff7+SzJ2qviuNMQdqlsDJkUM145xkGPVIfqWXq9Ui+2Dr6jdJWH5KYdynz9+4CzKjI5u6g==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. diff --git a/packages/webapp/public/locales/en/common.json b/packages/webapp/public/locales/en/common.json index 70b87244e8..48df07130c 100644 --- a/packages/webapp/public/locales/en/common.json +++ b/packages/webapp/public/locales/en/common.json @@ -41,6 +41,7 @@ "NOTES": "Notes", "OK": "OK", "OPTIONAL": "(optional)", + "OR": "or", "OTHER": "Other", "PAST": "Past", "PLANNED": "Planned", diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 6a2be0e542..9c2e502fc4 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -490,6 +490,13 @@ "LOCATION": "Location", "TASK": "Task" }, + "ERROR_FALLBACK": { + "CONTACT": "Still stuck in the mud? No worries, we're here to pull you out: <1>{{supportEmail}}", + "MAIN": "Sometimes LiteFarm gets lost and just needs a bit of help. One of these usually resolves the problem:", + "RELOAD": "Reload the page", + "SUBTITLE": "Don't worry, it's not you, it's us.", + "TITLE": "Oops! Seems like this page has wandered off!" + }, "EXPENSE": { "ADD_EXPENSE": { "ADD_CUSTOM_EXPENSE": "Add custom expense", @@ -839,6 +846,7 @@ "MANAGE_CUSTOM_TYPE": "Manage custom type", "REPORT": { "DATES": "Dates", + "FILE_TITLE": "Financial Report", "SETTINGS": "Export Settings", "TRANSACTION": "Transaction", "TRANSACTIONS": "Transactions" diff --git a/packages/webapp/public/locales/es/common.json b/packages/webapp/public/locales/es/common.json index 576d73ecdf..68b8de9b4d 100644 --- a/packages/webapp/public/locales/es/common.json +++ b/packages/webapp/public/locales/es/common.json @@ -41,6 +41,7 @@ "NOTES": "Notas", "OK": "Está bien", "OPTIONAL": "(opcional)", + "OR": "MISSING", "OTHER": "Otro", "PAST": "Pasado", "PLANNED": "Planificado", diff --git a/packages/webapp/public/locales/es/translation.json b/packages/webapp/public/locales/es/translation.json index a5bbeffb58..55653fe13e 100644 --- a/packages/webapp/public/locales/es/translation.json +++ b/packages/webapp/public/locales/es/translation.json @@ -489,6 +489,13 @@ "LOCATION": "ubicación", "TASK": "tarea" }, + "ERROR_FALLBACK": { + "CONTACT": "MISSING", + "MAIN": "MISSING", + "RELOAD": "MISSING", + "SUBTITLE": "MISSING", + "TITLE": "MISSING" + }, "EXPENSE": { "ADD_EXPENSE": { "ADD_CUSTOM_EXPENSE": "Agregar gasto personalizado", @@ -838,6 +845,7 @@ "MANAGE_CUSTOM_TYPE": "Administrar tipos personalizados", "REPORT": { "DATES": "Fechas", + "FILE_TITLE": "MISSING", "SETTINGS": "Configuración del reporte", "TRANSACTION": "Transacción", "TRANSACTIONS": "Transacciones" diff --git a/packages/webapp/public/locales/fr/common.json b/packages/webapp/public/locales/fr/common.json index 3282a7b372..4a38a5da6e 100644 --- a/packages/webapp/public/locales/fr/common.json +++ b/packages/webapp/public/locales/fr/common.json @@ -41,6 +41,7 @@ "NOTES": "Notes", "OK": "OK", "OPTIONAL": "(optionnel)", + "OR": "MISSING", "OTHER": "Autre", "PAST": "Passé", "PLANNED": "Planifié", diff --git a/packages/webapp/public/locales/fr/translation.json b/packages/webapp/public/locales/fr/translation.json index bdb6f0ed65..2be909fbd2 100644 --- a/packages/webapp/public/locales/fr/translation.json +++ b/packages/webapp/public/locales/fr/translation.json @@ -490,6 +490,13 @@ "LOCATION": "emplacement", "TASK": "tâche" }, + "ERROR_FALLBACK": { + "CONTACT": "Toujours embourbé ? Pas de souci, nous sommes là pour vous sortir de là:", + "MAIN": "Parfois, LiteFarm se perd et a juste besoin d'un peu d'aide. L'une de ces solutions résout généralement le problème:", + "RELOAD": "Rechargez la page", + "SUBTITLE": "Ne vous inquiétez pas, ce n'est pas de votre faute, c'est la nôtre.", + "TITLE": "Oups ! On dirait que cette page s'est égarée!" + }, "EXPENSE": { "ADD_EXPENSE": { "ADD_CUSTOM_EXPENSE": "Ajouter une dépense personnalisée", @@ -838,6 +845,7 @@ }, "REPORT": { "DATES": "Dates", + "FILE_TITLE": "MISSING", "SETTINGS": "Exporter les paramètres", "TRANSACTION": "Transaction", "TRANSACTIONS": "Transactions" @@ -1403,9 +1411,9 @@ "CLEAR_ALL": "Supprimer tout" }, "RELEASE": { - "BETTER": "MISSING", + "BETTER": "LiteFarm vient de s'améliorer!", "LITEFARM_UPDATED": "LiteFarm v{{version}} est maintenant disponible\u00a0!", - "NOTES": "MISSING" + "NOTES": "Notes de version" }, "REPEAT_PLAN": { "AFTER": "Après", diff --git a/packages/webapp/public/locales/pt/common.json b/packages/webapp/public/locales/pt/common.json index 987f572d00..e44d85e22a 100644 --- a/packages/webapp/public/locales/pt/common.json +++ b/packages/webapp/public/locales/pt/common.json @@ -41,6 +41,7 @@ "NOTES": "Notas", "OK": "Ok", "OPTIONAL": "(Opcional)", + "OR": "MISSING", "OTHER": "Outro", "PAST": "Passado", "PLANNED": "Planejado", diff --git a/packages/webapp/public/locales/pt/translation.json b/packages/webapp/public/locales/pt/translation.json index 2814797f3f..4d3df69ac9 100644 --- a/packages/webapp/public/locales/pt/translation.json +++ b/packages/webapp/public/locales/pt/translation.json @@ -489,6 +489,13 @@ "LOCATION": "localização", "TASK": "tarefa" }, + "ERROR_FALLBACK": { + "CONTACT": "MISSING", + "MAIN": "MISSING", + "RELOAD": "MISSING", + "SUBTITLE": "MISSING", + "TITLE": "MISSING" + }, "EXPENSE": { "ADD_EXPENSE": { "ADD_CUSTOM_EXPENSE": "Adicionar despesa personalizada", @@ -837,6 +844,7 @@ }, "REPORT": { "DATES": "Datas", + "FILE_TITLE": "MISSING", "SETTINGS": "Configurações de exportação", "TRANSACTION": "Transação", "TRANSACTIONS": "Transações" diff --git a/packages/webapp/src/assets/colors.scss b/packages/webapp/src/assets/colors.scss index 342f26b2e3..44cd5db605 100644 --- a/packages/webapp/src/assets/colors.scss +++ b/packages/webapp/src/assets/colors.scss @@ -65,4 +65,8 @@ --modalPrimary: white; --bgInputListTile: white; --mainBackground: #FAFCFB; + + // New design system colours + --Colors-Primary-Primary-teal-900: #16423d; + --Colors-Accent---singles-Brown-dark: #633700; } diff --git a/packages/webapp/src/assets/images/errorFallback/background.svg b/packages/webapp/src/assets/images/errorFallback/background.svg new file mode 100644 index 0000000000..63dc397f50 --- /dev/null +++ b/packages/webapp/src/assets/images/errorFallback/background.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webapp/src/assets/images/errorFallback/background_mobile.svg b/packages/webapp/src/assets/images/errorFallback/background_mobile.svg new file mode 100644 index 0000000000..5c1125d1c3 --- /dev/null +++ b/packages/webapp/src/assets/images/errorFallback/background_mobile.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/webapp/src/assets/images/errorFallback/farmer_desktop.svg b/packages/webapp/src/assets/images/errorFallback/farmer_desktop.svg new file mode 100644 index 0000000000..161c6e4649 --- /dev/null +++ b/packages/webapp/src/assets/images/errorFallback/farmer_desktop.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/webapp/src/assets/images/errorFallback/farmer_mobile.svg b/packages/webapp/src/assets/images/errorFallback/farmer_mobile.svg new file mode 100644 index 0000000000..d17bc57524 --- /dev/null +++ b/packages/webapp/src/assets/images/errorFallback/farmer_mobile.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/webapp/src/assets/images/errorFallback/logout.svg b/packages/webapp/src/assets/images/errorFallback/logout.svg new file mode 100644 index 0000000000..767987e896 --- /dev/null +++ b/packages/webapp/src/assets/images/errorFallback/logout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/webapp/src/assets/images/errorFallback/refresh.svg b/packages/webapp/src/assets/images/errorFallback/refresh.svg new file mode 100644 index 0000000000..d881f680d7 --- /dev/null +++ b/packages/webapp/src/assets/images/errorFallback/refresh.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/index.tsx b/packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/index.tsx new file mode 100644 index 0000000000..bf788e2e4b --- /dev/null +++ b/packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { useTranslation, Trans } from 'react-i18next'; +import { useTheme } from '@mui/styles'; +import { useMediaQuery } from '@mui/material'; +import styles from './styles.module.scss'; +import { Title, Main } from '../../Typography'; +import TextButton from '../../Form/Button/TextButton'; +import { ReactComponent as Background } from '../../../assets/images/errorFallback/background.svg'; +import { ReactComponent as MobileBackground } from '../../../assets/images/errorFallback/background_mobile.svg'; +import { ReactComponent as FarmerDesktop } from '../../../assets/images/errorFallback/farmer_desktop.svg'; +import { ReactComponent as FarmerMobile } from '../../../assets/images/errorFallback/farmer_mobile.svg'; +import { ReactComponent as RefreshIcon } from '../../../assets/images/errorFallback/refresh.svg'; +import { ReactComponent as LogoutIcon } from '../../../assets/images/errorFallback/logout.svg'; +import { ReactComponent as Logo } from '../../../assets/images/nav/logo-large.svg'; +import { SUPPORT_EMAIL } from '../../../util/constants'; + +interface PureReactErrorFallbackProps { + handleReload: () => Promise; + handleLogout: () => void; +} + +export const PureReactErrorFallback = ({ + handleReload, + handleLogout, +}: PureReactErrorFallbackProps) => { + const { t } = useTranslation(); + + const theme = useTheme(); + const isDesktop = useMediaQuery(theme.breakpoints.up('sm')); + + return ( +
+ {isDesktop ? ( + + ) : ( + + )} + +
+ {t('ERROR_FALLBACK.TITLE')} +
{t('ERROR_FALLBACK.SUBTITLE')}
+
{t('ERROR_FALLBACK.MAIN')}
+
+ + + {t('ERROR_FALLBACK.RELOAD')} + +
{t('common:OR')}
+ + + {t('PROFILE_FLOATER.LOG_OUT')} + +
+
+ , + }} + /> +
+
+ {isDesktop ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/styles.module.scss b/packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/styles.module.scss new file mode 100644 index 0000000000..bb58201a3c --- /dev/null +++ b/packages/webapp/src/components/ErrorHandler/PureReactErrorFallback/styles.module.scss @@ -0,0 +1,149 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +@import '../../../assets/mixin'; + +.container { + min-height: 100vh; + min-width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.background { + position: fixed; + background-color: var(--mainBackground, #fafcfb); + top: 56px; + z-index: -1; + + @include xs-breakpoint { + top: 0px; + width: 100vw; + padding-block: 48px; + } +} + +.logo { + height: 100px; + width: auto; + margin-block: 48px; + margin-left: 10%; + align-self: flex-start; + + @include xs-breakpoint { + height: 56px; + margin: 32px; + } +} + +.textContainer { + width: 90%; + max-width: 648px; + background-color: var(--mainBackground, #fafcfb); + padding: 32px; + + p { + font-family: 'Open Sans'; + line-height: normal; + font-style: normal; + font-size: 18px; + color: var(--grey900, #282b36); + letter-spacing: -0.396px; + } + + @include xs-breakpoint { + width: 100%; + padding-bottom: 56px; + + p { + font-size: 14px; + letter-spacing: -0.308px; + } + } +} + +.title { + color: var(--Colors-Primary-Primary-teal-900, #16423d); + text-align: center; + font-size: 28px; + font-weight: 600; + line-height: 48px; + letter-spacing: -0.616px; + + @include xs-breakpoint { + text-align: start; + font-size: 20px; + line-height: 32px; + letter-spacing: -0.44px; + } +} + +.subtitle, +.supportText { + font-weight: 600; +} + +.buttonContainer { + display: flex; + align-items: center; + gap: 24px; + padding-block: 32px; +} + +.iconLink { + display: flex; + align-items: center; + gap: 4px; + + font-size: 18px; + font-weight: 700; + color: var(--Colors-Primary-Primary-teal-900, #16423d); + + @include xs-breakpoint { + font-size: 16px; + } +} + +.icon { + flex: none; + height: 32px; + width: 32px; + + @include xs-breakpoint { + height: 24px; + } +} + +.or { + font-weight: 600; +} + +.email { + color: var(--Colors-Accent---singles-Brown-dark, #633700); + font-weight: 700; + text-decoration: none; +} + +.farmerDesktop { + height: 415px; +} + +.farmerMobile { + height: 304px; + align-self: end; + transform: translateY(-56px); +} diff --git a/packages/webapp/src/components/Finances/AddTransactionButton/index.jsx b/packages/webapp/src/components/Finances/AddTransactionButton/index.jsx index 135adaec86..a7c7719312 100644 --- a/packages/webapp/src/components/Finances/AddTransactionButton/index.jsx +++ b/packages/webapp/src/components/Finances/AddTransactionButton/index.jsx @@ -16,9 +16,10 @@ import { forwardRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; +import { useTheme } from '@mui/styles'; +import { useMediaQuery } from '@mui/material'; import { setPersistedPaths } from '../../../containers/hooks/useHookFormPersist/hookFormPersistSlice'; import history from '../../../history'; -import useIsAboveBreakpoint from '../../../hooks/useIsAboveBreakpoint'; import DropdownButton from '../../Form/DropDownButton'; import FloatingButtonMenu from '../../Menu/FloatingButtonMenu'; import FloatingMenu from '../../Menu/FloatingButtonMenu/FloatingMenu'; @@ -58,7 +59,8 @@ Menu.displayName = 'Menu'; export default function AddTransactionButton() { const { t } = useTranslation(); - const isAboveBreakPoint = useIsAboveBreakpoint(`(min-width: 856px)`); + const theme = useTheme(); + const isAboveBreakPoint = useMediaQuery(theme.breakpoints.up('md')); return ( <> diff --git a/packages/webapp/src/containers/ErrorHandler/ReactErrorFallback/index.tsx b/packages/webapp/src/containers/ErrorHandler/ReactErrorFallback/index.tsx new file mode 100644 index 0000000000..874d991345 --- /dev/null +++ b/packages/webapp/src/containers/ErrorHandler/ReactErrorFallback/index.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import * as Sentry from '@sentry/react'; +import { FallbackProps } from 'react-error-boundary'; +import { logout } from '../../../util/jwt'; +import { PureReactErrorFallback } from '../../../components/ErrorHandler/PureReactErrorFallback'; + +export default function ReactErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + const handleErrorAction = (action: string, error: Error) => { + Sentry.withScope((scope) => { + scope.addBreadcrumb({ + category: 'error-action', + message: `User chose to ${action} after an error`, + level: Sentry.Severity.Info, + }); + + scope.setTags({ fallback_user_action: action }); + + Sentry.captureException(error); + }); + }; + + const handleReload = async () => { + handleErrorAction('reload', error); + // Page reload interrupts the network request to Sentry, so we must wait for the Sentry action queue to complete (flush) + await Sentry.flush(); + window.location.reload(); + }; + + const handleLogout = () => { + handleErrorAction('logout', error); + resetErrorBoundary(); + logout(); + }; + + return ; +} diff --git a/packages/webapp/src/hooks/useIsAboveBreakpoint.js b/packages/webapp/src/hooks/useIsAboveBreakpoint.js deleted file mode 100644 index 1c07a2d944..0000000000 --- a/packages/webapp/src/hooks/useIsAboveBreakpoint.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023 LiteFarm.org - * This file is part of LiteFarm. - * - * LiteFarm is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LiteFarm is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details, see . - */ - -import { useEffect, useState } from 'react'; - -const useIsAboveBreakpoint = (mqString) => { - // this will get a boolean value later, but initialize with null so that - // a wrong component will not be shown initially - const [isAboveBreakPoint, setIsAboveBreakPoint] = useState(null); - - useEffect(() => { - const media = matchMedia(mqString); - - setIsAboveBreakPoint(media.matches); - - media.addEventListener('change', (e) => setIsAboveBreakPoint(e.matches)); - - return () => { - media.removeEventListener('change', setIsAboveBreakPoint); - }; - }, []); - - return isAboveBreakPoint; -}; - -export default useIsAboveBreakpoint; diff --git a/packages/webapp/src/main.jsx b/packages/webapp/src/main.jsx index 6bc3922ac1..a5a6d57d88 100644 --- a/packages/webapp/src/main.jsx +++ b/packages/webapp/src/main.jsx @@ -17,6 +17,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import * as Sentry from '@sentry/react'; import { Integrations } from '@sentry/tracing'; +import { ErrorBoundary } from 'react-error-boundary'; import { Router } from 'react-router-dom'; import history from './history'; import homeSaga from './containers/saga'; @@ -73,6 +74,7 @@ import abandonAndCompleteManagementPlanSaga from './containers/Crop/CompleteMana import notificationSaga from './containers/Notification/saga'; import errorHandlerSaga from './containers/ErrorHandler/saga'; import App from './App'; +import ReactErrorFallback from './containers/ErrorHandler/ReactErrorFallback/'; import { sagaMiddleware } from './store/sagaMiddleware'; import { persistor, store } from './store/store'; import { GlobalScss } from './components/GlobalScss'; @@ -153,11 +155,13 @@ ReactDOM.createRoot(document.getElementById('root')).render( - - <> - - - + + + <> + + + + diff --git a/packages/webapp/src/stories/ReactErrorFallback/ReactErrorFallback.stories.tsx b/packages/webapp/src/stories/ReactErrorFallback/ReactErrorFallback.stories.tsx new file mode 100644 index 0000000000..a803534770 --- /dev/null +++ b/packages/webapp/src/stories/ReactErrorFallback/ReactErrorFallback.stories.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { ReactNode } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { componentDecorators } from '../Pages/config/Decorators'; +import { PureReactErrorFallback } from '../../components/ErrorHandler/PureReactErrorFallback'; +import { Main } from '../../components/Typography'; +import styles from './styles.module.scss'; + +// https://storybook.js.org/docs/writing-stories/typescript +const meta: Meta = { + title: 'Components/PureReactErrorFallback', + component: PureReactErrorFallback, + decorators: [ + (Story) => ( + + + + ), + ...componentDecorators, + ], +}; +export default meta; + +interface ResizeWrapperProps { + children: ReactNode; +} + +const ResizeWrapper = ({ children }: ResizeWrapperProps) => { + return ( +
+
Resize window to see mobile / desktop view
+ {children} +
+ ); +}; + +type Story = StoryObj; + +export const Default: Story = { + args: { + handleReload: async () => { + console.log('page reload clicked'); + }, + handleLogout: () => { + console.log('logout clicked'); + }, + }, +}; diff --git a/packages/webapp/src/stories/ReactErrorFallback/styles.module.scss b/packages/webapp/src/stories/ReactErrorFallback/styles.module.scss new file mode 100644 index 0000000000..698a53e623 --- /dev/null +++ b/packages/webapp/src/stories/ReactErrorFallback/styles.module.scss @@ -0,0 +1,24 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +.wrapper { + position: relative; + transform: translateZ(0); // Create context for position: fixed + outline: 1px solid black; + + .note { + padding: 16px; + } +} diff --git a/packages/webapp/src/util/constants.js b/packages/webapp/src/util/constants.js index d0c94f7025..1caf1d5d8f 100644 --- a/packages/webapp/src/util/constants.js +++ b/packages/webapp/src/util/constants.js @@ -5,6 +5,7 @@ export const DO_ORIGIN_URL = `https://${ export const DO_CDN_URL = `https://${ import.meta.env.VITE_DO_BUCKET_NAME }.nyc3.cdn.digitaloceanspaces.com`; +export const SUPPORT_EMAIL = 'support@litefarm.org'; // Changing this forces logout and updates the new release card export const APP_VERSION = '3.6.0'; From 52e4078ea37e4c51dbc7fdff1d8e4c259a31a62b Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Fri, 9 Feb 2024 19:12:33 -0800 Subject: [PATCH 002/150] LF-4089 Add new colours And lint the scss file as I think only my editor does so --- packages/webapp/src/assets/colors.scss | 40 +++++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/webapp/src/assets/colors.scss b/packages/webapp/src/assets/colors.scss index 44cd5db605..cb4d6f5b2e 100644 --- a/packages/webapp/src/assets/colors.scss +++ b/packages/webapp/src/assets/colors.scss @@ -4,16 +4,16 @@ --teal600: #3ea992; --teal500: #89d1c7; --teal100: #f1fbf9; - --teal50: #EBF5F4; + --teal50: #ebf5f4; --green800: #048211; - --green500: #78C99E; - --green700: #558F70; + --green500: #78c99e; + --green700: #558f70; --green400: #a8e6bd; --green200: #c7efd3; --green100: #e3f8ec; --secondaryGreen800: #495c51; - --secondaryGreen200: #C1E6D2; - --secondaryGreen50: #F2FAF5; + --secondaryGreen200: #c1e6d2; + --secondaryGreen50: #f2faf5; --yellow700: #ffb800; --yellow400: #fed450; --yellow300: #fce38d; @@ -27,12 +27,12 @@ --overlay: rgba(36, 39, 48, 0.5); --red700: #d02620; --red400: #f58282; - --red200: #FFE8E8; + --red200: #ffe8e8; --orange700: #ffa73f; --orange400: #ffc888; --purple700: #8f26f0; --purple400: #ffe55b; - --brightGreen700: #037A0F; + --brightGreen700: #037a0f; --brightGreen400: #a6f7ae; --cyan700: #03a6ca; --cayn400: #4fdbfa; @@ -40,8 +40,8 @@ --blue700: #0669e1; --grey1: #333333; --brown200: #fff6ed; - --brown700: #AA5F04; - --brown900: #7E4C0E; + --brown700: #aa5f04; + --brown900: #7e4c0e; --info: var(--blue700); --success: var(--brightGreen700); --warning: var(--orange700); @@ -62,11 +62,23 @@ --border: var(--grey200); --checkbox: var(--teal700); --tooltipFont: var(--grey1); - --modalPrimary: white; + --modalPrimary: white; --bgInputListTile: white; - --mainBackground: #FAFCFB; + --mainBackground: #fafcfb; - // New design system colours - --Colors-Primary-Primary-teal-900: #16423d; - --Colors-Accent---singles-Brown-dark: #633700; + // New design system colours + --Colors-Primary-Primary-teal-50: #ebf5f4; + --Colors-Primary-Primary-teal-900: #16423d; + + --Colors-Secondary-Secondary-green-200: #c1e6d2; + --Colors-Secondary-Secondary-green-700: #558f70; + --Colors-Secondary-Secondary-green-900: #325442; + + --Colors-Neutral-Neutral-900: #2b303a; + + --Btn-primary-pristine: #ffcf54; + + --Colors-Accent-Accent-yellow-50: #fff8e6; + --Colors-Accent-Accent-yellow-400: #ffc633; + --Colors-Accent---singles-Brown-dark: #633700; } From f902150b6dc7829e0cb019792424e0c3fff2f686 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:19:09 +0000 Subject: [PATCH 003/150] Merge pull request #3171 from LiteFarmOrg/LF-4178-update-button-colours-and-variants-throughout-app LF-4178 Update button colours and variants throughout app --- packages/webapp/src/assets/colors.scss | 20 ++ .../webapp/src/components/Crop/Detail.jsx | 5 - .../components/Form/Button/button.module.scss | 215 ++++++++++++++---- .../src/components/Form/Button/index.tsx | 17 +- .../src/components/Profile/EditUser/index.jsx | 2 +- .../PureTaskTypeSelection.jsx | 2 +- .../src/stories/Button/Button.stories.jsx | 64 +++++- 7 files changed, 260 insertions(+), 65 deletions(-) diff --git a/packages/webapp/src/assets/colors.scss b/packages/webapp/src/assets/colors.scss index cb4d6f5b2e..7dcd8f78ca 100644 --- a/packages/webapp/src/assets/colors.scss +++ b/packages/webapp/src/assets/colors.scss @@ -67,6 +67,8 @@ --mainBackground: #fafcfb; // New design system colours + --White: #fff; + --Colors-Primary-Primary-teal-50: #ebf5f4; --Colors-Primary-Primary-teal-900: #16423d; @@ -74,11 +76,29 @@ --Colors-Secondary-Secondary-green-700: #558f70; --Colors-Secondary-Secondary-green-900: #325442; + --Colors-Neutral-Neutral-50: #f0f1f3; + --Colors-Neutral-Neutral-200: #b9bfc9; + --Colors-Neutral-Neutral-300: #98a1b1; + --Colors-Neutral-Neutral-400: #858fa1; + --Colors-Neutral-Neutral-500: #66738a; + --Colors-Neutral-Neutral-600: #5d697e; + --Colors-Neutral-Neutral-700: #485262; --Colors-Neutral-Neutral-900: #2b303a; --Btn-primary-pristine: #ffcf54; + --Btn-primary-hover: #e8a700; + --Btn-primary-disabled: #e7ebf2; --Colors-Accent-Accent-yellow-50: #fff8e6; + --Colors-Accent-Accent-yellow-100: #ffe9b0; --Colors-Accent-Accent-yellow-400: #ffc633; + --Colors-Accent-Accent-yellow-500: #ffb800; + --Colors-Accent-Accent-yellow-600: #e8a700; + --Colors-Accent-Accent-yellow-700: #b58300; + --Colors-Accent-Accent-yellow-900: #6b4d00; + --Colors-Accent---singles-Red-light: #ffdad9; + --Colors-Accent---singles-Red-full: #d02620; + --Colors-Accent---singles-Red-dark: #520f0d; + --Colors-Accent---singles-Blue-dark: #032d61; --Colors-Accent---singles-Brown-dark: #633700; } diff --git a/packages/webapp/src/components/Crop/Detail.jsx b/packages/webapp/src/components/Crop/Detail.jsx index 9bd29def77..d1b460e8df 100644 --- a/packages/webapp/src/components/Crop/Detail.jsx +++ b/packages/webapp/src/components/Crop/Detail.jsx @@ -75,11 +75,6 @@ function PureCropDetail({ }, ]} /> - - {/* */} )} {isEditing && ( diff --git a/packages/webapp/src/components/Form/Button/button.module.scss b/packages/webapp/src/components/Form/Button/button.module.scss index 0d09f6a2ad..1731337b22 100644 --- a/packages/webapp/src/components/Form/Button/button.module.scss +++ b/packages/webapp/src/components/Form/Button/button.module.scss @@ -1,92 +1,211 @@ +/* + * Copyright 2021-2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + @import '../../../assets/mixin'; -.primary { - background-color: var(--btnPrimary); - color: var(--fontColor); - text-decoration: none; - box-shadow: 0 2px 8px rgba(102, 115, 138, 0.3); - border: none; + +.btn.primary { + --background: var(--Btn-primary-pristine); + --border: none; + --box-shadow: 0px 1px 2px 0px var(--Colors-Neutral-Neutral-500); + --color: var(--Colors-Accent-Accent-yellow-900); + + --hover-background: var(--Btn-primary-hover); + --hover-box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.25); + --hover-color: var(--Colors-Accent-Accent-yellow-50); + + --active-background: var(--Colors-Accent-Accent-yellow-500); + --active-color: var(--Colors-Accent-Accent-yellow-900); + + --focus-border: 2px solid var(--Colors-Accent-Accent-yellow-600); } -.secondary { - background-color: var(--btnSecondary); - color: var(--labels); - border-color: var(--iconDefault); - border: 1px solid; - box-sizing: border-box; - text-decoration: none; +.btn.secondary { + --background: var(--White); + --border: 1px solid var(--Colors-Neutral-Neutral-200); + --color: var(--Colors-Neutral-Neutral-500); + + --hover-border: 1px solid var(--Colors-Neutral-Neutral-500); + --hover-box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.25); + --hover-color: var(--Colors-Neutral-Neutral-500); + + --active-border: 1px solid var(--Colors-Neutral-Neutral-900); + --active-color: var(--Colors-Neutral-Neutral-900); } -.success { - background-color: var(--btnSecondary); - color: var(--teal700); - border-color: var(--teal700); - border: 1px solid; - box-sizing: border-box; - text-decoration: none; - box-shadow: 0px 2px 4px rgba(102, 115, 138, 0.3); +.btn.secondary-2 { + --background: var(--White); + --border: 1px solid var(--Colors-Primary-Primary-teal-600); + --color: var(--Colors-Primary-Primary-teal-600); + + --hover-background: var(--White); + --hover-border: 1px solid var(--Colors-Primary-Primary-teal-900); + --hover-box-shadow: 0px 1px 2px 0px var(--Colors-Neutral-Neutral-500); + --hover-color: var(--Colors-Primary-Primary-teal-900); + + --active-background: var(--Colors-Secondary-Secondary-green-100); + --active-border: 1px solid var(--Colors-Primary-Primary-teal-600); + --active-color: var(--Colors-Primary-Primary-teal-600); } -.error { - background-color: var(--error); - color: white; - text-decoration: none; - box-shadow: 0 2px 8px rgba(102, 115, 138, 0.3); - border: none; +.btn.secondary-cta { + --background: none; + --border: none; + --color: var(--Colors-Accent-Accent-yellow-900); + + --hover-background: var(--Colors-Accent-Accent-yellow-50); + --hover-box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.25); + --hover-color: var(--Colors-Accent-Accent-yellow-700); + + --active-background: var(--Colors-Accent-Accent-yellow-700); + --active-color: var(--Colors-Accent-Accent-yellow-100); + + --focus-border: 2px solid var(--Colors-Accent-Accent-yellow-600); + --focus-box-shadow: none; } -.warning { - background-color: var(--brown700); - color: white; - text-decoration: none; - box-shadow: 0 2px 8px rgba(102, 115, 138, 0.3); - border: none; +.btn.error { + --background: var(--error); + --color: var(--White); + --box-shadow: 0 2px 8px rgba(102, 115, 138, 0.3); + --border: none; + + /* These states not yet defined for this variant */ + --hover-background: var(--background); + --hover-border: var(--border); + --hover-color: var(--color); + + --active-background: var(--background); + --active-border: var(--border); + --active-color: var(--color); } +.btn.warning { + --background: var(--brown700); + --color: var(--White); + --box-shadow: 0 2px 8px rgba(102, 115, 138, 0.3); + --border: none; + + /* These states not yet defined for this variant */ + --hover-background: var(--background); + --hover-border: var(--border); + --hover-color: var(--color); + + --active-background: var(--background); + --active-border: var(--border); + --active-color: var(--color); +} .btn { - border-radius: 4px; - font-weight: 600; - size: 16px; - line-height: 24px; + border-radius: 8px; min-height: 48px; padding: 0 16px; + font-size: 16px; - cursor: pointer; + font-family: 'Open Sans', 'SansSerif', serif; + font-weight: 600; + line-height: 24px; + letter-spacing: 0.4px; + text-decoration: none; + display: flex; align-items: center; justify-content: center; - font-family: 'Open Sans', 'SansSerif', serif; + gap: 8px; + + cursor: pointer; + + /* Used in all but secondary-cta variant */ + --focus-box-shadow: 6px -6px 0px 0px var(--Colors-Secondary-Secondary-green-200), + -6px 6px 0px 0px var(--Colors-Secondary-Secondary-green-200), + -6px -6px 0px 0px var(--Colors-Secondary-Secondary-green-200), + 6px 6px 0px 0px var(--Colors-Secondary-Secondary-green-200); + + /* Based on variant */ + background: var(--background); + border: var(--border); + box-shadow: var(--box-shadow); + color: var(--color); +} + +.btn svg path { + stroke: var(--color); +} + +.btn:hover:enabled { + background: var(--hover-background); + border: var(--hover-border); + box-shadow: var(--hover-box-shadow); + color: var(--hover-color); +} + +.btn:hover:enabled svg path { + stroke: var(--hover-color); +} + +.btn:active:enabled { + background: var(--active-background); + border: var(--active-border); + box-shadow: none; + color: var(--active-color); +} + +.btn:active:enabled svg path { + stroke: var(--active-color); } .btn:disabled { border: none; - color: var(--inputDisabled); - background-color: var(--btnDisabled); + color: var(--Colors-Neutral-Neutral-300); + background: var(--Btn-primary-disabled); box-shadow: none; cursor: default; } -.primary:hover:enabled { - background-color: var(--btnHoverPrimary); - box-shadow: 0px 4px 12px rgba(102, 115, 138, 0.4); +.btn:disabled svg path { + stroke: var(--Colors-Neutral-Neutral-300); } -.secondary:hover:enabled { - border-color: var(--labels); +.btn.error { + --background: var(--error); + --color: white; + --box-shadow: 0 2px 8px rgba(102, 115, 138, 0.3); + --border: none; } -.fullLength { - width: 100%; +.btn.warning { + --background: var(--brown700); + --color: white; + --box-shadow: 0 2px 8px rgba(102, 115, 138, 0.3); + --border: none; } .btn:focus { outline: none; + border: var(--focus-border); + box-shadow: var(--focus-box-shadow); +} + +.fullLength { + width: 100%; } .sm { font-size: 14px; min-height: 32px; padding: 0 16px; + gap: 4px; } .textButton { diff --git a/packages/webapp/src/components/Form/Button/index.tsx b/packages/webapp/src/components/Form/Button/index.tsx index eb3ee2b3b4..d70cf3b38b 100644 --- a/packages/webapp/src/components/Form/Button/index.tsx +++ b/packages/webapp/src/components/Form/Button/index.tsx @@ -1,9 +1,24 @@ +/* + * Copyright 2022-2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + import React, { ReactNode } from 'react'; import styles from './button.module.scss'; import clsx from 'clsx'; type ButtonProps = { - color?: 'primary' | 'secondary' | 'success' | 'error' | 'warning' | 'none'; + color?: 'primary' | 'secondary' | 'secondary-2' | 'secondary-cta' | 'warning' | 'error' | 'none'; children?: ReactNode; sm?: boolean; disabled?: boolean; diff --git a/packages/webapp/src/components/Profile/EditUser/index.jsx b/packages/webapp/src/components/Profile/EditUser/index.jsx index cc301f36d4..66ca5fb6c5 100644 --- a/packages/webapp/src/components/Profile/EditUser/index.jsx +++ b/packages/webapp/src/components/Profile/EditUser/index.jsx @@ -140,7 +140,7 @@ export default function PureEditUser({ buttonGroup={ <> {userFarm.status === 'Inactive' ? ( - ) : ( diff --git a/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx b/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx index 97d1339b9d..539404e7f4 100644 --- a/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx +++ b/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx @@ -175,7 +175,7 @@ export const PureTaskTypeSelection = ({ })} {isAdmin && ( - )} diff --git a/packages/webapp/src/stories/Button/Button.stories.jsx b/packages/webapp/src/stories/Button/Button.stories.jsx index fddee966ad..c658de8eb2 100644 --- a/packages/webapp/src/stories/Button/Button.stories.jsx +++ b/packages/webapp/src/stories/Button/Button.stories.jsx @@ -1,6 +1,7 @@ import React from 'react'; import Button from '../../components/Form/Button'; import { componentDecorators } from '../Pages/config/Decorators'; +import { ReactComponent as EditIcon } from '../../assets/images/edit.svg'; export default { title: 'Components/Button', @@ -9,6 +10,7 @@ export default { }; const Template = (args) => + ); +} diff --git a/packages/webapp/src/components/Form/NumberInput/index.tsx b/packages/webapp/src/components/Form/NumberInput/index.tsx new file mode 100644 index 0000000000..e7eba9b4f9 --- /dev/null +++ b/packages/webapp/src/components/Form/NumberInput/index.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { ReactNode } from 'react'; +import InputBase, { type InputBaseSharedProps } from '../InputBase'; +import NumberInputStepper from './NumberInputStepper'; +import useNumberInput, { NumberInputOptions } from './useNumberInput'; +import { UseControllerProps, useController } from 'react-hook-form'; + +export type NumberInputProps = NumberInputOptions & { + /** + * The currency symbol to display on left side of input + */ + currencySymbol?: ReactNode; + /** + * The unit to display on right side of input + */ + unit?: ReactNode; + /** + * Controls visibility of stepper. + */ + showStepper?: boolean; +} & InputBaseSharedProps; + +type RhfProps = { + name: string; + control: UseControllerProps['control']; + rules?: UseControllerProps['rules']; +}; + +export default function NumberInput({ + initialValue, + locale, + useGrouping = true, + allowDecimal = true, + decimalDigits, + unit, + currencySymbol, + step = 1, + max = Infinity, + min = 0, + showStepper = false, + name, + control, + rules, + onChange, + onBlur, + ...props +}: NumberInputProps & RhfProps) { + const { field, fieldState } = useController({ name, control, rules, defaultValue: initialValue }); + const { inputProps, reset, numericValue, increment, decrement } = useNumberInput({ + initialValue, + allowDecimal, + decimalDigits, + locale, + useGrouping, + step, + min, + max, + onChange: (value) => { + field.onChange(isNaN(value) ? null : value); + onChange?.(value); + }, + onBlur: () => { + field.onBlur(); + onBlur?.(); + }, + }); + + return ( + + {unit} + {showStepper && ( + + )} + + } + /> + ); +} diff --git a/packages/webapp/src/components/Form/NumberInput/stepper.module.scss b/packages/webapp/src/components/Form/NumberInput/stepper.module.scss new file mode 100644 index 0000000000..a5ae4d81ef --- /dev/null +++ b/packages/webapp/src/components/Form/NumberInput/stepper.module.scss @@ -0,0 +1,52 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +.stepper { + display: flex; + flex-direction: column; + gap: 2px; + height: 24px; + width: 24px; +} + +.stepperBtnUnstyled { + all: unset; + cursor: pointer; + + &:disabled { + cursor: not-allowed; + } +} + +.stepperBtn { + background-color: var(--Colors-Neutral-Neutral-50); + color: var(--Colors-Neutral-Neutral-600); + flex: 1; + display: flex; + justify-content: center; + align-items: center; + + &:first-child { + border-top-right-radius: 3px; + } + &:last-child { + border-bottom-right-radius: 3px; + } + + &:disabled { + background-color: #f9fafc; + color: #dadee5; + } +} diff --git a/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts b/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts new file mode 100644 index 0000000000..7bd96322e9 --- /dev/null +++ b/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts @@ -0,0 +1,197 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { useTranslation } from 'react-i18next'; +import { clamp, countDecimalPlaces, createNumberFormatter } from './utils'; +import { ChangeEvent, ComponentPropsWithRef, useMemo, useRef, useState } from 'react'; + +export type NumberInputOptions = { + /** + * Value shown on first render. + */ + initialValue?: number | null; + /** + * Controls grouping of numbers over 1000 with the thousands separator. + */ + useGrouping?: boolean; + /** + * Controls whether or not a decimal is allowed as input. If set to false, users can only enter whole numbers. + */ + allowDecimal?: boolean; + /** + * The locale to use for number formatting. + */ + locale?: string; + /** + * Number of decimal digits shown after blur. + */ + decimalDigits?: number; + /** + * - Amount to increment or decrement. + * - If allowDecimal is false, then step is rounded to the nearest whole number. + */ + step?: number; + /** + * - Maximum value of input. + * - If input value is greater than max then input value is clamped to max on blur. + * - If input value equals max then incrementing with stepper and keyboard is disabled. + */ + max?: number; + /** + * - Minimum value of input. + * - If input value is less than min then input value is clamped to min on blur. + * - If input value equals min then decrementing with stepper and keyboard is disabled. + */ + min?: number; + /** + * Function called when number value of input changes. + * @param value - Current value represented as number or NaN if input field is empty. + */ + onChange?: (value: number) => void; + /** + * Function called when input is blurred. + */ + onBlur?: () => void; +}; + +export default function useNumberInput({ + initialValue, + locale: customLocale, + decimalDigits, + allowDecimal, + useGrouping, + step = 1, + min = 0, + max = Infinity, + onChange, + onBlur, +}: NumberInputOptions) { + const { + i18n: { language }, + } = useTranslation(); + + const locale = customLocale || language; + + const formatter = useMemo(() => { + const stepDecimalPlaces = countDecimalPlaces(step); + const options: Intl.NumberFormatOptions = { + useGrouping, + minimumFractionDigits: !allowDecimal ? undefined : decimalDigits ?? stepDecimalPlaces, + maximumFractionDigits: !allowDecimal ? 0 : decimalDigits ?? (stepDecimalPlaces || 20), + }; + + return createNumberFormatter(locale, options); + }, [locale, useGrouping, decimalDigits, step, allowDecimal]); + + const { decimalSeparator, thousandsSeparator } = useMemo(() => { + let separators = { + decimalSeparator: '.', + thousandsSeparator: ',', + }; + + // 11000.2 - random decimal number over 1000 used to extract thousands and decimal separator + const numberParts = createNumberFormatter(locale).formatToParts(11000.2); + for (let { type, value } of numberParts) { + if (type === 'decimal') { + separators.decimalSeparator = value; + } else if (type === 'group') { + separators.thousandsSeparator = value; + } + } + return separators; + }, [locale]); + + const [numericValue, setNumericValue] = useState(initialValue ?? NaN); + + // current input value that is focused and has been touched + const [touchedValue, setTouchedValue] = useState(''); + const [isFocused, setIsFocused] = useState(false); + const inputRef = useRef(null); + const stepValue = allowDecimal ? step : Math.round(step); + + const update = (next: number) => { + setNumericValue(next); + onChange?.(next); + }; + + const handleChange = (e: ChangeEvent) => { + const { value, validity } = e.target; + if (validity.patternMismatch) return; + setTouchedValue(value); + update(parseFloat(decimalSeparator === '.' ? value : value.replace(decimalSeparator, '.'))); + }; + + const handleBlur = () => { + if (numericValue < min || numericValue > max) { + update(clamp(numericValue, min, max)); + } + setIsFocused(false); + setTouchedValue(''); + onBlur?.(); + }; + + const pattern = useMemo(() => { + if (!isFocused) return; + if (!allowDecimal) return '[0-9]+'; + const decimalSeparatorRegex = `[${decimalSeparator === '.' ? '.' : `${decimalSeparator}.`}]`; + return `[0-9]*${decimalSeparatorRegex}?[0-9]*`; + }, [isFocused, allowDecimal, decimalSeparator]); + + const getDisplayValue = () => { + if (isNaN(numericValue)) return ''; + if (isFocused) + return touchedValue || formatter.format(numericValue).replaceAll(thousandsSeparator, ''); + return formatter.format(numericValue); + }; + + const handleStep = (next: number) => { + // focus input when clicking on up/down button + if (!isFocused) inputRef.current?.focus(); + if (touchedValue) setTouchedValue(''); + update(clamp(next, Math.max(min, 0), max)); + }; + + const increment = () => handleStep((numericValue || 0) + stepValue); + const decrement = () => handleStep((numericValue || 0) - stepValue); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowUp') { + // prevent cursor from shifting to start of input + e.preventDefault(); + increment(); + } else if (e.key === 'ArrowDown') { + decrement(); + } + }; + + const inputProps: ComponentPropsWithRef<'input'> = { + inputMode: 'decimal', + value: getDisplayValue(), + pattern, + onChange: handleChange, + onBlur: handleBlur, + onFocus: () => setIsFocused(true), + onKeyDown: handleKeyDown, + ref: inputRef, + }; + + return { + numericValue, + inputProps, + reset: () => update(initialValue ?? NaN), + increment, + decrement, + }; +} diff --git a/packages/webapp/src/components/Form/NumberInput/utils.ts b/packages/webapp/src/components/Form/NumberInput/utils.ts new file mode 100644 index 0000000000..138ea18efc --- /dev/null +++ b/packages/webapp/src/components/Form/NumberInput/utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +export function countDecimalPlaces(number: number) { + if (!Number.isFinite(number)) return 0; + let e = 1; + let decimalPlaces = 0; + while (Math.round(number * e) / e !== number) { + e *= 10; + decimalPlaces += 1; + } + return decimalPlaces; +} + +export function clamp(value: number, min: number, max: number) { + if (max < min) console.warn('clamp: max cannot be less than min'); + + return Math.min(Math.max(value, min), max); +} + +export function createNumberFormatter(locale: string, options?: Intl.NumberFormatOptions) { + try { + return new Intl.NumberFormat(locale, options); + } catch (error) { + // undefined will use browsers best matching locale + return new Intl.NumberFormat(undefined, options); + } +} diff --git a/packages/webapp/src/components/Icons/cross/index.jsx b/packages/webapp/src/components/Icons/cross/index.jsx deleted file mode 100644 index 5b82ab0670..0000000000 --- a/packages/webapp/src/components/Icons/cross/index.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import styles from './cross.module.scss'; -import clsx from 'clsx'; -import PropTypes from 'prop-types'; - -const Cross = ({ className, onClick, ...props }) => { - return ( - - × - - ); -}; - -Cross.propTypes = { - onClick: PropTypes.func, - className: PropTypes.string, -}; - -export default Cross; diff --git a/packages/webapp/src/components/Icons/cross/index.tsx b/packages/webapp/src/components/Icons/cross/index.tsx new file mode 100644 index 0000000000..30858ca814 --- /dev/null +++ b/packages/webapp/src/components/Icons/cross/index.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import React from 'react'; +import styles from './cross.module.scss'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; + +type CrossProps = { + className?: string; + isClickable?: boolean; + onClick?: () => void; +}; + +const Cross = ({ className, onClick, isClickable, ...props }: CrossProps) => { + return ( + + × + + ); +}; + +Cross.propTypes = { + onClick: PropTypes.func, + className: PropTypes.string, +}; + +export default Cross; diff --git a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx new file mode 100644 index 0000000000..7bb6cf059d --- /dev/null +++ b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx @@ -0,0 +1,361 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import NumberInputRHF from '../../../components/Form/NumberInput'; +import { componentDecorators } from '../../Pages/config/Decorators'; +import { userEvent, within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +import { FormProvider, useForm } from 'react-hook-form'; + +const meta: Meta = { + title: 'Components/NumberInput', + component: NumberInputRHF, + args: { + name: 'test', + }, + decorators: [ + ...componentDecorators, + (Story) => { + const methods = useForm({ mode: 'onChange' }); + return ( + + + + ); + }, + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + play: async ({ step }) => { + await step( + 'Enter invalid characters', + test('aD@j)+-ec&', { expectValue: '', expectValueOnBlur: '', expectValueOnReFocus: '' }), + ); + await step( + 'Enter invalid and valid characters', + test('1a4D@^,8).+-ec3.&4', { + expectValue: '148.34', + expectValueOnBlur: '148.34', + expectValueOnReFocus: '148.34', + }), + ); + await step( + 'Enter negative number', + test('-22', { expectValue: '22', expectValueOnBlur: '22', expectValueOnReFocus: '22' }), + ); + + // test handling of multiple thousands seperators + await step( + 'Enter number above 1,000,000', + test('1556398', { + expectValue: '1556398', + expectValueOnBlur: '1,556,398', + expectValueOnReFocus: '1556398', + }), + ); + + await step( + 'Enter number with leading zeroes', + test('00078', { expectValue: '00078', expectValueOnBlur: '78', expectValueOnReFocus: '78' }), + ); + }, +}; + +export const WithLocale: Story = { + args: { locale: 'pt', info: 'Invalid locale defaults to browsers locale' }, + render: (args) => { + return ; + }, + argTypes: { locale: { description: 'Overrides locale used internally with i18n' } }, + play: async ({ step }) => { + // should be able to enter numbers in specified locale + await step( + 'Enter number with locale decimal separator', + test('7498,431', { + expectValue: '7498,431', + expectValueOnBlur: '7.498,431', + expectValueOnReFocus: '7498,431', + }), + ); + + // should be able to enter english numbers + // should format to localized number on blur + await step( + 'Enter number with decimal period', + test('7498.431', { + expectValue: '7498.431', + expectValueOnBlur: '7.498,431', + expectValueOnReFocus: '7498,431', + }), + ); + }, +}; + +export const WithoutGrouping: Story = { + args: { useGrouping: false }, + play: async ({ step, canvasElement }) => { + //should not insert thousands separator + await step( + 'Enter whole number above 1000', + test('122492', { + expectValue: '122492', + expectValueOnBlur: '122492', + expectValueOnReFocus: '122492', + }), + ); + await step( + 'Enter decimal number above 1000', + test('7642.1', { + expectValue: '7642.1', + expectValueOnBlur: '7642.1', + expectValueOnReFocus: '7642.1', + }), + ); + }, +}; + +export const WithoutDecimal: Story = { + args: { allowDecimal: false }, + play: async ({ step }) => { + await step( + 'Enter number with decimal', + test('9.1', { expectValue: '91', expectValueOnBlur: '91', expectValueOnReFocus: '91' }), + ); + }, +}; +export const WithoutDecimalAndWithFractionalStep: Story = { + args: { allowDecimal: false, step: 1.7 }, + play: async ({ canvasElement }) => { + const input = getInput(canvasElement); + + await userEvent.click(input); + await userEvent.keyboard('{ArrowUp}'); + // should round step value up to nearest whole number + expect(input).toHaveValue('2'); + await userEvent.keyboard('{ArrowDown}'); + expect(input).toHaveValue('0'); + await userEvent.keyboard('{ArrowDown}'); + expect(input).toHaveValue('0'); + userEvent.clear(input); + }, +}; + +export const WithDecimalDigits: Story = { + args: { decimalDigits: 2 }, + play: async ({ step }) => { + await step( + 'Enter number with more than 2 decimal places', + test('8.5751', { + expectValue: '8.5751', + expectValueOnBlur: '8.58', + expectValueOnReFocus: '8.58', + }), + ); + + await step( + 'Enter number with less than 2 decimal places', + test('8', { + expectValue: '8', + expectValueOnBlur: '8.00', + expectValueOnReFocus: '8.00', + }), + ); + }, +}; + +export const WithInitialValue: Story = { + args: { initialValue: 33 }, +}; + +export const Unit: Story = { + args: { + unit: 'kg', + }, +}; + +export const Currency: Story = { + args: { + currencySymbol: '$', + decimalDigits: 2, + }, +}; + +export const Stepper: Story = { + args: { + step: 0.1, + showStepper: true, + }, + play: async ({ canvasElement }) => { + const input = getInput(canvasElement); + const { incrementButton, decrementButton } = getStepperButtons(canvasElement); + + await expect(incrementButton).toBeInTheDocument(); + await expect(decrementButton).toBeInTheDocument(); + await userEvent.click(incrementButton!); + expect(input).toHaveValue('0.1'); + await userEvent.click(decrementButton!); + expect(input).toHaveValue('0.0'); + expect(decrementButton).toBeDisabled(); + + await userEvent.keyboard('{ArrowUp}'); + await userEvent.keyboard('{ArrowUp}'); + expect(input).toHaveValue('0.2'); + + await userEvent.keyboard('{ArrowDown}'); + expect(input).toHaveValue('0.1'); + userEvent.clear(input); + + // should work after entering value without blurring + userEvent.type(input, '5645'); + await userEvent.keyboard('{ArrowUp}'); + expect(input).toHaveValue('5645.1'); + userEvent.clear(input); + }, +}; + +export const StepperDisabled: Story = { + args: { + showStepper: true, + step: 0.1, + disabled: true, + }, +}; +export const StepperWithMinMax: Story = { + args: { + step: 1, + min: 7, + max: 14, + showStepper: true, + }, + play: async ({ canvasElement, args, step }) => { + const input = getInput(canvasElement); + const { incrementButton, decrementButton } = getStepperButtons(canvasElement); + await expect(incrementButton).toBeInTheDocument(); + await expect(decrementButton).toBeInTheDocument(); + + expect(input).toHaveValue(''); + // should clamp to min when clicking stepper and current value is below min + await userEvent.click(incrementButton!); + expect(input).toHaveValue('7'); + expect(decrementButton).toBeDisabled(); + + // increment to max + let value = 7; + while (value !== args.max) { + await userEvent.click(incrementButton!); + expect(input).toHaveValue((value + args.step!).toString()); + value++; + } + expect(incrementButton).toBeDisabled(); + userEvent.clear(input); + + // should clamp to max on blur when entering value above max + await step( + 'Enter value above max', + test('2566', { + expectValue: '2566', + expectValueOnBlur: '14', + expectValueOnReFocus: '14', + }), + ); + + // should clamp to min on blur when entering value below min + await step( + 'Enter value below min', + test('2', { + expectValue: '2', + expectValueOnBlur: '7', + expectValueOnReFocus: '7', + }), + ); + }, +}; + +export const WithError: Story = { + args: { + rules: { + max: { value: 10, message: 'Error - number should be below 10.' }, + }, + info: 'Enter number above 10 to trigger error', + }, +}; + +export const ErrorWithUnitAndStepper: Story = { + args: { + showStepper: true, + unit: 'kg', + step: 3, + rules: { + max: { value: 10, message: 'Error - number should be below 10.' }, + }, + info: 'Enter number above 10 to trigger error', + }, +}; + +export const WithOptionalLabel: Story = { + args: { + label: 'A label', + optional: true, + }, +}; + +function test( + value: string, + { + expectValue, + expectValueOnBlur, + expectValueOnReFocus, + }: { expectValue: string; expectValueOnBlur: string; expectValueOnReFocus: string }, +): NonNullable { + return async ({ canvasElement }) => { + const input = getInput(canvasElement); + + // enter value + await userEvent.click(input); + await userEvent.type(input, value); + expect(input).toHaveValue(expectValue); + + //blur + await userEvent.tab(); + expect(input).toHaveValue(expectValueOnBlur); + + //reFocus + await userEvent.click(input); + expect(input).toHaveValue(expectValueOnReFocus); + + await userEvent.clear(input); + }; +} + +function getInput(canvasElement: HTMLElement) { + return within(canvasElement).getByRole('textbox'); +} + +function getStepperButtons(canvasElement: HTMLElement) { + const canvas = within(canvasElement); + const incrementButton = canvas.queryByRole('button', { name: 'increase' }); + const decrementButton = canvas.queryByRole('button', { name: 'decrease' }); + + return { + incrementButton, + decrementButton, + }; +} diff --git a/packages/webapp/src/tests/numberInputUtils.test.js b/packages/webapp/src/tests/numberInputUtils.test.js new file mode 100644 index 0000000000..43b13a76c1 --- /dev/null +++ b/packages/webapp/src/tests/numberInputUtils.test.js @@ -0,0 +1,68 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { expect, describe, test } from 'vitest'; +import { countDecimalPlaces, clamp } from '../components/Form/NumberInput/utils'; + +describe('numberInput utility tests', () => { + describe('countDecimalPlaces', () => { + test(`should return correct number of decimal places for positive numbers`, () => { + expect(countDecimalPlaces(1.11)).toBe(2); + expect(countDecimalPlaces(2.1)).toBe(1); + expect(countDecimalPlaces(0.00001)).toBe(5); + }); + + test(`should return correct number of decimal places for negative numbers`, () => { + expect(countDecimalPlaces(-6)).toBe(0); + expect(countDecimalPlaces(-1.1)).toBe(1); + expect(countDecimalPlaces(-1.11)).toBe(2); + }); + + test(`should return 0 for whole numbers`, () => { + expect(countDecimalPlaces(1)).toBe(0); + expect(countDecimalPlaces(0)).toBe(0); + expect(countDecimalPlaces(2300)).toBe(0); + expect(countDecimalPlaces(92.0)).toBe(0); + }); + + test(`should return 0 non-finite numbers`, () => { + expect(countDecimalPlaces(Infinity)).toBe(0); + expect(countDecimalPlaces(-Infinity)).toBe(0); + expect(countDecimalPlaces(NaN)).toBe(0); + }); + + test(`should return 0 non-numeric values`, () => { + expect(countDecimalPlaces(undefined)).toBe(0); + expect(countDecimalPlaces(null)).toBe(0); + expect(countDecimalPlaces('11.384')).toBe(0); + expect(countDecimalPlaces(true)).toBe(0); + expect(countDecimalPlaces(false)).toBe(0); + expect(countDecimalPlaces([])).toBe(0); + expect(countDecimalPlaces({})).toBe(0); + }); + }); + + describe('clamp', () => { + test('should return min when value is less than min', () => { + expect(clamp(2, 4, 7)).toBe(4); + }); + test('should return max when value is greater than max', () => { + expect(clamp(13, 4, 7)).toBe(7); + }); + test('should return value when value is between min and max', () => { + expect(clamp(5, 4, 7)).toBe(5); + }); + }); +}); From df575b81e031025666bd5de99a122d0e317e8848 Mon Sep 17 00:00:00 2001 From: Navdeep Date: Wed, 3 Apr 2024 22:15:24 -0400 Subject: [PATCH 005/150] LF-4154 add mainSection prop to InputBase --- .../Form/InputBase/InputBaseField/index.tsx | 17 ++++++++--------- .../src/components/Form/InputBase/index.tsx | 2 ++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx b/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx index dd994cd9b0..d8918ad0eb 100644 --- a/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx +++ b/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx @@ -21,11 +21,12 @@ import { HTMLInputProps } from '..'; export type InputBaseFieldProps = { resetIcon?: ReactElement; leftSection?: ReactNode; + mainSection?: ReactNode; rightSection?: ReactNode; } & HTMLInputProps; const InputBaseField = forwardRef((props, ref) => { - const { resetIcon, leftSection, rightSection, ...inputProps } = props; + const { resetIcon, leftSection, mainSection, rightSection, ...inputProps } = props; const showResetIcon = !!resetIcon; return (
((props, inputProps.disabled && styles.inputDisabled, )} > - {props.leftSection && ( -
- {props.leftSection} -
+ {leftSection && ( +
{leftSection}
)} - - {(showResetIcon || props.rightSection) && ( + {mainSection || } + {(showResetIcon || rightSection) && (
- {props.rightSection} - {props.resetIcon} + {rightSection} + {resetIcon}
)}
diff --git a/packages/webapp/src/components/Form/InputBase/index.tsx b/packages/webapp/src/components/Form/InputBase/index.tsx index 3b8214bd6a..393338c700 100644 --- a/packages/webapp/src/components/Form/InputBase/index.tsx +++ b/packages/webapp/src/components/Form/InputBase/index.tsx @@ -47,6 +47,7 @@ const InputBase = forwardRef((props, ref) => { link, icon, leftSection, + mainSection, rightSection, onResetIconClick, classes, @@ -68,6 +69,7 @@ const InputBase = forwardRef((props, ref) => { : undefined} ref={ref} From 89d668d0fd518cd7d6ba81dd64342d0cb3be9bd8 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 24 Jun 2024 13:47:42 -0700 Subject: [PATCH 006/150] LF-4154 add clampOnBlur option for number input * cherry-pick be23eb6, remove change in SexDetailsCountInput --- .../src/components/Form/NumberInput/index.tsx | 2 ++ .../Form/NumberInput/useNumberInput.ts | 7 +++++- .../Form/NumberInput/NumberInput.stories.tsx | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Form/NumberInput/index.tsx b/packages/webapp/src/components/Form/NumberInput/index.tsx index e7eba9b4f9..2d1fab75c6 100644 --- a/packages/webapp/src/components/Form/NumberInput/index.tsx +++ b/packages/webapp/src/components/Form/NumberInput/index.tsx @@ -52,6 +52,7 @@ export default function NumberInput({ max = Infinity, min = 0, showStepper = false, + clampOnBlur = true, name, control, rules, @@ -69,6 +70,7 @@ export default function NumberInput({ step, min, max, + clampOnBlur, onChange: (value) => { field.onChange(isNaN(value) ? null : value); onChange?.(value); diff --git a/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts b/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts index 7bd96322e9..6a59f2acb8 100644 --- a/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts +++ b/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts @@ -55,6 +55,10 @@ export type NumberInputOptions = { * - If input value equals min then decrementing with stepper and keyboard is disabled. */ min?: number; + /** + * Controls whether or not to clamp value on blur that is outside of allowed range + */ + clampOnBlur?: boolean; /** * Function called when number value of input changes. * @param value - Current value represented as number or NaN if input field is empty. @@ -75,6 +79,7 @@ export default function useNumberInput({ step = 1, min = 0, max = Infinity, + clampOnBlur = true, onChange, onBlur, }: NumberInputOptions) { @@ -134,7 +139,7 @@ export default function useNumberInput({ }; const handleBlur = () => { - if (numericValue < min || numericValue > max) { + if (clampOnBlur && (numericValue < min || numericValue > max)) { update(clamp(numericValue, min, max)); } setIsFocused(false); diff --git a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx index 7bb6cf059d..22ae15addd 100644 --- a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx +++ b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx @@ -317,6 +317,28 @@ export const WithOptionalLabel: Story = { }, }; +export const WithoutClampOnBlur: Story = { + args: { + min: 9, + max: 20, + clampOnBlur: false, + }, + play: async ({ step }) => { + await step( + 'Enter number below min', + test('8', { expectValue: '8', expectValueOnBlur: '8', expectValueOnReFocus: '8' }), + ); + await step( + 'Enter number above max', + test('99', { + expectValue: '99', + expectValueOnBlur: '99', + expectValueOnReFocus: '99', + }), + ); + }, +}; + function test( value: string, { From 3b352abf586432853d3494ef0bec5e17743864ac Mon Sep 17 00:00:00 2001 From: Navdeep Date: Sat, 6 Apr 2024 10:46:38 -0400 Subject: [PATCH 007/150] LF-4154 clear number input field with 0 value when focusing --- .../src/components/Form/NumberInput/useNumberInput.ts | 6 +++++- .../src/stories/Form/NumberInput/NumberInput.stories.tsx | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts b/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts index 6a59f2acb8..feaadd9554 100644 --- a/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts +++ b/packages/webapp/src/components/Form/NumberInput/useNumberInput.ts @@ -157,7 +157,11 @@ export default function useNumberInput({ const getDisplayValue = () => { if (isNaN(numericValue)) return ''; if (isFocused) - return touchedValue || formatter.format(numericValue).replaceAll(thousandsSeparator, ''); + return ( + touchedValue || + (numericValue && formatter.format(numericValue).replaceAll(thousandsSeparator, '')) || + '' + ); return formatter.format(numericValue); }; diff --git a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx index 22ae15addd..82cc99057e 100644 --- a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx +++ b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx @@ -76,6 +76,11 @@ export const Default: Story = { 'Enter number with leading zeroes', test('00078', { expectValue: '00078', expectValueOnBlur: '78', expectValueOnReFocus: '78' }), ); + + await step( + 'Enter zero value', + test('0000', { expectValue: '0000', expectValueOnBlur: '0', expectValueOnReFocus: '' }), + ); }, }; From f123bf39a818164df2befc8cab7014262b99ea64 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 24 Jun 2024 13:52:26 -0700 Subject: [PATCH 008/150] LF-4154 add showResetIcon prop * cherry-pick b469ee9, remove changes in SexDetailsCountInput and SexDetails --- .../Form/InputBase/InputBaseField/index.tsx | 11 ++++++----- .../webapp/src/components/Form/InputBase/index.tsx | 12 ++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx b/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx index d8918ad0eb..b281815fdc 100644 --- a/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx +++ b/packages/webapp/src/components/Form/InputBase/InputBaseField/index.tsx @@ -19,20 +19,21 @@ import clsx from 'clsx'; import { HTMLInputProps } from '..'; export type InputBaseFieldProps = { - resetIcon?: ReactElement; leftSection?: ReactNode; mainSection?: ReactNode; rightSection?: ReactNode; + isError?: boolean; + resetIcon?: ReactElement; } & HTMLInputProps; const InputBaseField = forwardRef((props, ref) => { - const { resetIcon, leftSection, mainSection, rightSection, ...inputProps } = props; - const showResetIcon = !!resetIcon; + const { isError, resetIcon, leftSection, mainSection, rightSection, ...inputProps } = props; + return (
@@ -40,7 +41,7 @@ const InputBaseField = forwardRef((props,
{leftSection}
)} {mainSection || } - {(showResetIcon || rightSection) && ( + {(!!resetIcon || rightSection) && (
{rightSection} {resetIcon} diff --git a/packages/webapp/src/components/Form/InputBase/index.tsx b/packages/webapp/src/components/Form/InputBase/index.tsx index 393338c700..0559e777c0 100644 --- a/packages/webapp/src/components/Form/InputBase/index.tsx +++ b/packages/webapp/src/components/Form/InputBase/index.tsx @@ -23,8 +23,10 @@ import type { InputBaseFieldProps } from './InputBaseField'; import type { InputBaseLabelProps } from './InputBaseLabel'; export type HTMLInputProps = ComponentPropsWithoutRef<'input'>; + // props meant to be shared with other similar input components export type InputBaseSharedProps = InputBaseLabelProps & { + showResetIcon?: boolean; onResetIconClick?: () => void; info?: string; error?: string; @@ -33,7 +35,9 @@ export type InputBaseSharedProps = InputBaseLabelProps & { classes?: Record<'input' | 'label' | 'container' | 'info' | 'errors', React.CSSProperties>; } & Pick; -type InputBaseProps = InputBaseSharedProps & InputBaseFieldProps & HTMLInputProps; +type InputBaseProps = InputBaseSharedProps & + Pick & + HTMLInputProps; const InputBase = forwardRef((props, ref) => { const { @@ -49,6 +53,7 @@ const InputBase = forwardRef((props, ref) => { leftSection, mainSection, rightSection, + showResetIcon = true, onResetIconClick, classes, ...inputProps @@ -71,7 +76,10 @@ const InputBase = forwardRef((props, ref) => { leftSection={leftSection} mainSection={mainSection} rightSection={rightSection} - resetIcon={!!error ? : undefined} + isError={!!error} + resetIcon={ + !!error && showResetIcon ? : undefined + } ref={ref} /> From 83764852ee529f61fd26af862c625c9f4ff624d0 Mon Sep 17 00:00:00 2001 From: Navdeep Date: Mon, 8 Apr 2024 17:18:11 -0400 Subject: [PATCH 009/150] LF-4154 fix broken number input tests that resulted from making field empty when value is 0 --- .../src/stories/Form/NumberInput/NumberInput.stories.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx index 82cc99057e..cf70f3672d 100644 --- a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx +++ b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx @@ -146,6 +146,7 @@ export const WithoutDecimal: Story = { ); }, }; + export const WithoutDecimalAndWithFractionalStep: Story = { args: { allowDecimal: false, step: 1.7 }, play: async ({ canvasElement }) => { @@ -156,9 +157,9 @@ export const WithoutDecimalAndWithFractionalStep: Story = { // should round step value up to nearest whole number expect(input).toHaveValue('2'); await userEvent.keyboard('{ArrowDown}'); - expect(input).toHaveValue('0'); + expect(input).toHaveValue(''); await userEvent.keyboard('{ArrowDown}'); - expect(input).toHaveValue('0'); + expect(input).toHaveValue(''); userEvent.clear(input); }, }; @@ -217,7 +218,7 @@ export const Stepper: Story = { await userEvent.click(incrementButton!); expect(input).toHaveValue('0.1'); await userEvent.click(decrementButton!); - expect(input).toHaveValue('0.0'); + expect(input).toHaveValue(''); expect(decrementButton).toBeDisabled(); await userEvent.keyboard('{ArrowUp}'); From 0816328a040b04c5a95ac0d123fd9bd7b78380c7 Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 24 Jun 2024 13:59:53 -0700 Subject: [PATCH 010/150] LF-4154 add cursor: default to InputBaseField * add the change in 184e211 --- .../components/Form/InputBase/InputBaseField/styles.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webapp/src/components/Form/InputBase/InputBaseField/styles.module.scss b/packages/webapp/src/components/Form/InputBase/InputBaseField/styles.module.scss index 169d9fd157..a2d0a5f616 100644 --- a/packages/webapp/src/components/Form/InputBase/InputBaseField/styles.module.scss +++ b/packages/webapp/src/components/Form/InputBase/InputBaseField/styles.module.scss @@ -68,6 +68,7 @@ input:focus::placeholder { justify-content: center; align-items: center; gap: 8px; + cursor: default; &Left { padding-right: 6px; From 5c18487d4d681c9142e07ed2b8495b47b9f9ae9b Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 24 Jun 2024 14:03:20 -0700 Subject: [PATCH 011/150] LF-4156 fix ts error by updating NumberInput controller props * cherry-pick 08d3fc8, remove changes in AddAnimalsFormCard --- .../src/components/Form/NumberInput/index.tsx | 48 +++++++++---------- .../Form/NumberInput/NumberInput.stories.tsx | 2 +- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/webapp/src/components/Form/NumberInput/index.tsx b/packages/webapp/src/components/Form/NumberInput/index.tsx index 2d1fab75c6..7d3d1dc5f4 100644 --- a/packages/webapp/src/components/Form/NumberInput/index.tsx +++ b/packages/webapp/src/components/Form/NumberInput/index.tsx @@ -17,31 +17,26 @@ import { ReactNode } from 'react'; import InputBase, { type InputBaseSharedProps } from '../InputBase'; import NumberInputStepper from './NumberInputStepper'; import useNumberInput, { NumberInputOptions } from './useNumberInput'; -import { UseControllerProps, useController } from 'react-hook-form'; +import { FieldValues, UseControllerProps, useController } from 'react-hook-form'; -export type NumberInputProps = NumberInputOptions & { - /** - * The currency symbol to display on left side of input - */ - currencySymbol?: ReactNode; - /** - * The unit to display on right side of input - */ - unit?: ReactNode; - /** - * Controls visibility of stepper. - */ - showStepper?: boolean; -} & InputBaseSharedProps; +export type NumberInputProps = UseControllerProps & + InputBaseSharedProps & + Omit & { + /** + * The currency symbol to display on left side of input + */ + currencySymbol?: ReactNode; + /** + * The unit to display on right side of input + */ + unit?: ReactNode; + /** + * Controls visibility of stepper. + */ + showStepper?: boolean; + }; -type RhfProps = { - name: string; - control: UseControllerProps['control']; - rules?: UseControllerProps['rules']; -}; - -export default function NumberInput({ - initialValue, +export default function NumberInput({ locale, useGrouping = true, allowDecimal = true, @@ -56,13 +51,14 @@ export default function NumberInput({ name, control, rules, + defaultValue, onChange, onBlur, ...props -}: NumberInputProps & RhfProps) { - const { field, fieldState } = useController({ name, control, rules, defaultValue: initialValue }); +}: NumberInputProps) { + const { field, fieldState } = useController({ name, control, rules, defaultValue }); const { inputProps, reset, numericValue, increment, decrement } = useNumberInput({ - initialValue, + initialValue: defaultValue, allowDecimal, decimalDigits, locale, diff --git a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx index cf70f3672d..e0e3541138 100644 --- a/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx +++ b/packages/webapp/src/stories/Form/NumberInput/NumberInput.stories.tsx @@ -188,7 +188,7 @@ export const WithDecimalDigits: Story = { }; export const WithInitialValue: Story = { - args: { initialValue: 33 }, + args: { defaultValue: 33 }, }; export const Unit: Story = { From 6175118f53a4c876aae363d00b42f887b2f65caa Mon Sep 17 00:00:00 2001 From: Sayaka Ono Date: Mon, 24 Jun 2024 14:06:46 -0700 Subject: [PATCH 012/150] LF-4156 add className prop to NumberInput and InputBase * cherry-pick 7549d0d, remove changes in AddAnimalsFormCard and AddAnimalsFormCard scss --- packages/webapp/src/components/Form/InputBase/index.tsx | 4 +++- .../src/components/Form/NumberInput/NumberInputStepper.tsx | 1 + packages/webapp/src/components/Form/NumberInput/index.tsx | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/components/Form/InputBase/index.tsx b/packages/webapp/src/components/Form/InputBase/index.tsx index 0559e777c0..812ff2f138 100644 --- a/packages/webapp/src/components/Form/InputBase/index.tsx +++ b/packages/webapp/src/components/Form/InputBase/index.tsx @@ -21,6 +21,7 @@ import { Error, Info, TextWithExternalLink } from '../../Typography'; import { Cross } from '../../Icons'; import type { InputBaseFieldProps } from './InputBaseField'; import type { InputBaseLabelProps } from './InputBaseLabel'; +import clsx from 'clsx'; export type HTMLInputProps = ComponentPropsWithoutRef<'input'>; @@ -56,11 +57,12 @@ const InputBase = forwardRef((props, ref) => { showResetIcon = true, onResetIconClick, classes, + className, ...inputProps } = props; return ( -
+
+ ); +}) as CreateableSelect; + +export { CreatableSelect }; diff --git a/packages/webapp/src/components/Form/ReactSelect/Select.tsx b/packages/webapp/src/components/Form/ReactSelect/Select.tsx new file mode 100644 index 0000000000..1c00bc074f --- /dev/null +++ b/packages/webapp/src/components/Form/ReactSelect/Select.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2024 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import React from 'react'; +import ReactSelect, { SelectInstance, Props, GroupBase } from 'react-select'; +import { useTranslation } from 'react-i18next'; +import InputBaseLabel, { type InputBaseLabelProps } from '../InputBase/InputBaseLabel'; +import { styles } from './styles'; +import scss from './styles.module.scss'; +import { ClearIndicator, MultiValueRemove } from './components'; + +type SelectProps< + Option = unknown, + IsMulti extends boolean = false, + Group extends GroupBase