realtimex-crm 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +104 -0
- package/dist/assets/DealList-DqDrFeDV.js +59 -0
- package/dist/assets/DealList-DqDrFeDV.js.map +1 -0
- package/dist/assets/index-BiQoGq1P.css +1 -0
- package/dist/assets/index-CDIy4x-0.js +152 -0
- package/dist/assets/index-CDIy4x-0.js.map +1 -0
- package/dist/auth-callback.html +140 -0
- package/dist/favicon.ico +0 -0
- package/dist/img/adding-users.png +0 -0
- package/dist/img/empty.svg +42 -0
- package/dist/index.html +1 -0
- package/dist/logo192.png +0 -0
- package/dist/logo512.png +0 -0
- package/dist/logos/0.png +0 -0
- package/dist/logos/1.png +0 -0
- package/dist/logos/10.png +0 -0
- package/dist/logos/11.png +0 -0
- package/dist/logos/12.png +0 -0
- package/dist/logos/13.png +0 -0
- package/dist/logos/14.png +0 -0
- package/dist/logos/15.png +0 -0
- package/dist/logos/16.png +0 -0
- package/dist/logos/17.png +0 -0
- package/dist/logos/18.png +0 -0
- package/dist/logos/19.png +0 -0
- package/dist/logos/2.png +0 -0
- package/dist/logos/20.png +0 -0
- package/dist/logos/21.png +0 -0
- package/dist/logos/22.png +0 -0
- package/dist/logos/23.png +0 -0
- package/dist/logos/24.png +0 -0
- package/dist/logos/25.png +0 -0
- package/dist/logos/26.png +0 -0
- package/dist/logos/27.png +0 -0
- package/dist/logos/28.png +0 -0
- package/dist/logos/29.png +0 -0
- package/dist/logos/3.png +0 -0
- package/dist/logos/30.png +0 -0
- package/dist/logos/31.png +0 -0
- package/dist/logos/32.png +0 -0
- package/dist/logos/33.png +0 -0
- package/dist/logos/34.png +0 -0
- package/dist/logos/35.png +0 -0
- package/dist/logos/36.png +0 -0
- package/dist/logos/37.png +0 -0
- package/dist/logos/38.png +0 -0
- package/dist/logos/39.png +0 -0
- package/dist/logos/4.png +0 -0
- package/dist/logos/40.png +0 -0
- package/dist/logos/41.png +0 -0
- package/dist/logos/42.png +0 -0
- package/dist/logos/43.png +0 -0
- package/dist/logos/44.png +0 -0
- package/dist/logos/45.png +0 -0
- package/dist/logos/46.png +0 -0
- package/dist/logos/47.png +0 -0
- package/dist/logos/48.png +0 -0
- package/dist/logos/49.png +0 -0
- package/dist/logos/5.png +0 -0
- package/dist/logos/50.png +0 -0
- package/dist/logos/51.png +0 -0
- package/dist/logos/52.png +0 -0
- package/dist/logos/53.png +0 -0
- package/dist/logos/54.png +0 -0
- package/dist/logos/55.png +0 -0
- package/dist/logos/6.png +0 -0
- package/dist/logos/7.png +0 -0
- package/dist/logos/8.png +0 -0
- package/dist/logos/9.png +0 -0
- package/dist/logos/Readme.md +1 -0
- package/dist/logos/logo_atomic_crm.svg +14 -0
- package/dist/logos/logo_atomic_crm_dark.svg +14 -0
- package/dist/logos/logo_atomic_crm_light.svg +14 -0
- package/dist/manifest.json +25 -0
- package/dist/robots.txt +3 -0
- package/dist/stats.html +4949 -0
- package/package.json +152 -0
- package/public/auth-callback.html +140 -0
- package/public/favicon.ico +0 -0
- package/public/img/adding-users.png +0 -0
- package/public/img/empty.svg +42 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/logos/0.png +0 -0
- package/public/logos/1.png +0 -0
- package/public/logos/10.png +0 -0
- package/public/logos/11.png +0 -0
- package/public/logos/12.png +0 -0
- package/public/logos/13.png +0 -0
- package/public/logos/14.png +0 -0
- package/public/logos/15.png +0 -0
- package/public/logos/16.png +0 -0
- package/public/logos/17.png +0 -0
- package/public/logos/18.png +0 -0
- package/public/logos/19.png +0 -0
- package/public/logos/2.png +0 -0
- package/public/logos/20.png +0 -0
- package/public/logos/21.png +0 -0
- package/public/logos/22.png +0 -0
- package/public/logos/23.png +0 -0
- package/public/logos/24.png +0 -0
- package/public/logos/25.png +0 -0
- package/public/logos/26.png +0 -0
- package/public/logos/27.png +0 -0
- package/public/logos/28.png +0 -0
- package/public/logos/29.png +0 -0
- package/public/logos/3.png +0 -0
- package/public/logos/30.png +0 -0
- package/public/logos/31.png +0 -0
- package/public/logos/32.png +0 -0
- package/public/logos/33.png +0 -0
- package/public/logos/34.png +0 -0
- package/public/logos/35.png +0 -0
- package/public/logos/36.png +0 -0
- package/public/logos/37.png +0 -0
- package/public/logos/38.png +0 -0
- package/public/logos/39.png +0 -0
- package/public/logos/4.png +0 -0
- package/public/logos/40.png +0 -0
- package/public/logos/41.png +0 -0
- package/public/logos/42.png +0 -0
- package/public/logos/43.png +0 -0
- package/public/logos/44.png +0 -0
- package/public/logos/45.png +0 -0
- package/public/logos/46.png +0 -0
- package/public/logos/47.png +0 -0
- package/public/logos/48.png +0 -0
- package/public/logos/49.png +0 -0
- package/public/logos/5.png +0 -0
- package/public/logos/50.png +0 -0
- package/public/logos/51.png +0 -0
- package/public/logos/52.png +0 -0
- package/public/logos/53.png +0 -0
- package/public/logos/54.png +0 -0
- package/public/logos/55.png +0 -0
- package/public/logos/6.png +0 -0
- package/public/logos/7.png +0 -0
- package/public/logos/8.png +0 -0
- package/public/logos/9.png +0 -0
- package/public/logos/Readme.md +1 -0
- package/public/logos/logo_atomic_crm.svg +14 -0
- package/public/logos/logo_atomic_crm_dark.svg +14 -0
- package/public/logos/logo_atomic_crm_light.svg +14 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/App.css +42 -0
- package/src/App.tsx +58 -0
- package/src/assets/react.svg +1 -0
- package/src/components/admin/Readme.md +40 -0
- package/src/components/admin/admin.tsx +132 -0
- package/src/components/admin/app-sidebar.tsx +166 -0
- package/src/components/admin/array-field.tsx +59 -0
- package/src/components/admin/array-input.tsx +201 -0
- package/src/components/admin/authentication.tsx +86 -0
- package/src/components/admin/autocomplete-array-input.tsx +254 -0
- package/src/components/admin/autocomplete-input.tsx +296 -0
- package/src/components/admin/badge-field.tsx +65 -0
- package/src/components/admin/boolean-input.tsx +116 -0
- package/src/components/admin/breadcrumb.tsx +135 -0
- package/src/components/admin/bulk-actions-toolbar.tsx +83 -0
- package/src/components/admin/bulk-delete-button.tsx +70 -0
- package/src/components/admin/bulk-export-button.tsx +76 -0
- package/src/components/admin/cancel-button.tsx +46 -0
- package/src/components/admin/columns-button.tsx +345 -0
- package/src/components/admin/confirm.tsx +166 -0
- package/src/components/admin/count.tsx +94 -0
- package/src/components/admin/create-button.tsx +58 -0
- package/src/components/admin/create.tsx +132 -0
- package/src/components/admin/data-table.tsx +520 -0
- package/src/components/admin/date-field.tsx +136 -0
- package/src/components/admin/date-input.tsx +317 -0
- package/src/components/admin/date-time-input.tsx +331 -0
- package/src/components/admin/delete-button.tsx +113 -0
- package/src/components/admin/edit-button.tsx +64 -0
- package/src/components/admin/edit-guesser.tsx +157 -0
- package/src/components/admin/edit.tsx +152 -0
- package/src/components/admin/email-field.tsx +74 -0
- package/src/components/admin/error.tsx +111 -0
- package/src/components/admin/export-button.tsx +126 -0
- package/src/components/admin/field-toggle.tsx +164 -0
- package/src/components/admin/file-field.tsx +123 -0
- package/src/components/admin/file-input.tsx +361 -0
- package/src/components/admin/filter-form.tsx +510 -0
- package/src/components/admin/form.tsx +312 -0
- package/src/components/admin/icon-button-with-tooltip.tsx +85 -0
- package/src/components/admin/index.ts +73 -0
- package/src/components/admin/input-helper-text.tsx +29 -0
- package/src/components/admin/layout.tsx +69 -0
- package/src/components/admin/list-guesser.tsx +239 -0
- package/src/components/admin/list-pagination.tsx +247 -0
- package/src/components/admin/list.tsx +178 -0
- package/src/components/admin/loading.tsx +40 -0
- package/src/components/admin/locales-menu-button.tsx +60 -0
- package/src/components/admin/login-page.tsx +104 -0
- package/src/components/admin/notification.tsx +114 -0
- package/src/components/admin/number-field.tsx +84 -0
- package/src/components/admin/number-input.tsx +124 -0
- package/src/components/admin/radio-button-group-input.tsx +184 -0
- package/src/components/admin/ready.tsx +55 -0
- package/src/components/admin/record-field.tsx +132 -0
- package/src/components/admin/reference-array-field.tsx +152 -0
- package/src/components/admin/reference-array-input.tsx +68 -0
- package/src/components/admin/reference-field.tsx +153 -0
- package/src/components/admin/reference-input.tsx +46 -0
- package/src/components/admin/reference-many-count.tsx +92 -0
- package/src/components/admin/reference-many-field.tsx +132 -0
- package/src/components/admin/refresh-button.tsx +31 -0
- package/src/components/admin/saved-queries.tsx +174 -0
- package/src/components/admin/search-input.tsx +57 -0
- package/src/components/admin/select-field.tsx +111 -0
- package/src/components/admin/select-input.tsx +323 -0
- package/src/components/admin/show-button.tsx +57 -0
- package/src/components/admin/show-guesser.tsx +215 -0
- package/src/components/admin/show.tsx +184 -0
- package/src/components/admin/simple-form-iterator.tsx +582 -0
- package/src/components/admin/simple-form.tsx +95 -0
- package/src/components/admin/simple-show-layout.tsx +8 -0
- package/src/components/admin/single-field-list.tsx +67 -0
- package/src/components/admin/sort-button.tsx +152 -0
- package/src/components/admin/spinner.tsx +46 -0
- package/src/components/admin/text-field.tsx +60 -0
- package/src/components/admin/text-input.tsx +77 -0
- package/src/components/admin/theme-mode-toggle.tsx +48 -0
- package/src/components/admin/theme-provider.tsx +74 -0
- package/src/components/admin/toggle-filter-button.tsx +77 -0
- package/src/components/admin/url-field.tsx +83 -0
- package/src/components/admin/user-menu.tsx +84 -0
- package/src/components/atomic-crm/activity/ActivityLog.tsx +54 -0
- package/src/components/atomic-crm/activity/ActivityLogCompanyCreated.tsx +50 -0
- package/src/components/atomic-crm/activity/ActivityLogContactCreated.tsx +42 -0
- package/src/components/atomic-crm/activity/ActivityLogContactNoteCreated.tsx +71 -0
- package/src/components/atomic-crm/activity/ActivityLogContext.tsx +11 -0
- package/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx +41 -0
- package/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx +84 -0
- package/src/components/atomic-crm/activity/ActivityLogIterator.tsx +80 -0
- package/src/components/atomic-crm/activity/ActivityLogNote.tsx +36 -0
- package/src/components/atomic-crm/companies/AutocompleteCompanyInput.tsx +43 -0
- package/src/components/atomic-crm/companies/CompanyAside.tsx +207 -0
- package/src/components/atomic-crm/companies/CompanyAvatar.tsx +29 -0
- package/src/components/atomic-crm/companies/CompanyCard.tsx +88 -0
- package/src/components/atomic-crm/companies/CompanyCreate.tsx +41 -0
- package/src/components/atomic-crm/companies/CompanyEdit.tsx +33 -0
- package/src/components/atomic-crm/companies/CompanyEmpty.tsx +26 -0
- package/src/components/atomic-crm/companies/CompanyInputs.tsx +160 -0
- package/src/components/atomic-crm/companies/CompanyList.tsx +54 -0
- package/src/components/atomic-crm/companies/CompanyListFilter.tsx +55 -0
- package/src/components/atomic-crm/companies/CompanyShow.tsx +241 -0
- package/src/components/atomic-crm/companies/GridList.tsx +46 -0
- package/src/components/atomic-crm/companies/index.ts +11 -0
- package/src/components/atomic-crm/companies/sizes.ts +7 -0
- package/src/components/atomic-crm/consts.ts +5 -0
- package/src/components/atomic-crm/contacts/Avatar.tsx +40 -0
- package/src/components/atomic-crm/contacts/ContactAside.tsx +187 -0
- package/src/components/atomic-crm/contacts/ContactCreate.tsx +34 -0
- package/src/components/atomic-crm/contacts/ContactEdit.tsx +32 -0
- package/src/components/atomic-crm/contacts/ContactEmpty.tsx +28 -0
- package/src/components/atomic-crm/contacts/ContactImportButton.tsx +213 -0
- package/src/components/atomic-crm/contacts/ContactInputs.tsx +209 -0
- package/src/components/atomic-crm/contacts/ContactList.tsx +116 -0
- package/src/components/atomic-crm/contacts/ContactListContent.tsx +107 -0
- package/src/components/atomic-crm/contacts/ContactListFilter.tsx +126 -0
- package/src/components/atomic-crm/contacts/ContactMergeButton.tsx +263 -0
- package/src/components/atomic-crm/contacts/ContactShow.tsx +76 -0
- package/src/components/atomic-crm/contacts/ExportVCardButton.tsx +79 -0
- package/src/components/atomic-crm/contacts/TagsList.tsx +33 -0
- package/src/components/atomic-crm/contacts/TagsListEdit.tsx +155 -0
- package/src/components/atomic-crm/contacts/contacts_export.csv +3 -0
- package/src/components/atomic-crm/contacts/exportToVCard.ts +104 -0
- package/src/components/atomic-crm/contacts/index.tsx +14 -0
- package/src/components/atomic-crm/contacts/useContactImport.tsx +206 -0
- package/src/components/atomic-crm/dashboard/Dashboard.tsx +66 -0
- package/src/components/atomic-crm/dashboard/DashboardActivityLog.tsx +22 -0
- package/src/components/atomic-crm/dashboard/DashboardStepper.tsx +72 -0
- package/src/components/atomic-crm/dashboard/DealsChart.tsx +202 -0
- package/src/components/atomic-crm/dashboard/DealsPipeline.tsx +90 -0
- package/src/components/atomic-crm/dashboard/HotContacts.tsx +92 -0
- package/src/components/atomic-crm/dashboard/LatestNotes.tsx +116 -0
- package/src/components/atomic-crm/dashboard/TasksList.tsx +69 -0
- package/src/components/atomic-crm/dashboard/TasksListEmpty.tsx +22 -0
- package/src/components/atomic-crm/dashboard/TasksListFilter.tsx +72 -0
- package/src/components/atomic-crm/dashboard/Welcome.tsx +41 -0
- package/src/components/atomic-crm/deals/ContactList.tsx +31 -0
- package/src/components/atomic-crm/deals/DealArchivedList.tsx +105 -0
- package/src/components/atomic-crm/deals/DealCard.tsx +78 -0
- package/src/components/atomic-crm/deals/DealColumn.tsx +52 -0
- package/src/components/atomic-crm/deals/DealCreate.tsx +95 -0
- package/src/components/atomic-crm/deals/DealEdit.tsx +81 -0
- package/src/components/atomic-crm/deals/DealEmpty.tsx +63 -0
- package/src/components/atomic-crm/deals/DealInputs.tsx +103 -0
- package/src/components/atomic-crm/deals/DealList.tsx +95 -0
- package/src/components/atomic-crm/deals/DealListContent.tsx +245 -0
- package/src/components/atomic-crm/deals/DealShow.tsx +260 -0
- package/src/components/atomic-crm/deals/OnlyMineInput.tsx +30 -0
- package/src/components/atomic-crm/deals/deal.ts +5 -0
- package/src/components/atomic-crm/deals/dealUtils.ts +26 -0
- package/src/components/atomic-crm/deals/index.ts +6 -0
- package/src/components/atomic-crm/deals/stages.ts +28 -0
- package/src/components/atomic-crm/filters/FilterCategory.tsx +20 -0
- package/src/components/atomic-crm/layout/FormToolbar.tsx +12 -0
- package/src/components/atomic-crm/layout/Header.tsx +134 -0
- package/src/components/atomic-crm/layout/Layout.tsx +21 -0
- package/src/components/atomic-crm/layout/TopToolbar.tsx +24 -0
- package/src/components/atomic-crm/login/LoginSkeleton.tsx +18 -0
- package/src/components/atomic-crm/login/SignupPage.tsx +150 -0
- package/src/components/atomic-crm/login/StartPage.tsx +27 -0
- package/src/components/atomic-crm/misc/AsideSection.tsx +21 -0
- package/src/components/atomic-crm/misc/ContactOption.tsx +26 -0
- package/src/components/atomic-crm/misc/ImageEditorField.tsx +206 -0
- package/src/components/atomic-crm/misc/RelativeDate.tsx +5 -0
- package/src/components/atomic-crm/misc/Status.tsx +28 -0
- package/src/components/atomic-crm/misc/fetchWithTimeout.ts +19 -0
- package/src/components/atomic-crm/misc/isLinkedInUrl.ts +15 -0
- package/src/components/atomic-crm/misc/unsupportedDomains.const.ts +105 -0
- package/src/components/atomic-crm/misc/useAppBarHeight.ts +9 -0
- package/src/components/atomic-crm/misc/usePapaParse.tsx +144 -0
- package/src/components/atomic-crm/notes/Note.tsx +187 -0
- package/src/components/atomic-crm/notes/NoteAttachments.tsx +56 -0
- package/src/components/atomic-crm/notes/NoteCreate.tsx +112 -0
- package/src/components/atomic-crm/notes/NoteInputs.tsx +92 -0
- package/src/components/atomic-crm/notes/NotesIterator.tsx +37 -0
- package/src/components/atomic-crm/notes/StatusSelector.tsx +39 -0
- package/src/components/atomic-crm/notes/index.ts +3 -0
- package/src/components/atomic-crm/notes/utils.ts +13 -0
- package/src/components/atomic-crm/providers/commons/activity.ts +174 -0
- package/src/components/atomic-crm/providers/commons/canAccess.ts +26 -0
- package/src/components/atomic-crm/providers/commons/getCompanyAvatar.spec.ts +20 -0
- package/src/components/atomic-crm/providers/commons/getCompanyAvatar.ts +21 -0
- package/src/components/atomic-crm/providers/commons/getContactAvatar.spec.ts +80 -0
- package/src/components/atomic-crm/providers/commons/getContactAvatar.ts +70 -0
- package/src/components/atomic-crm/providers/commons/mergeContacts.ts +185 -0
- package/src/components/atomic-crm/providers/fakerest/authProvider.ts +74 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/companies.ts +53 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/contactNotes.ts +25 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/contacts.ts +103 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/dealNotes.ts +19 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/deals.ts +53 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/finalize.ts +10 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/index.ts +25 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/sales.ts +37 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/tags.ts +14 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/tasks.ts +55 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/types.ts +21 -0
- package/src/components/atomic-crm/providers/fakerest/dataGenerator/utils.ts +28 -0
- package/src/components/atomic-crm/providers/fakerest/dataProvider.ts +518 -0
- package/src/components/atomic-crm/providers/fakerest/index.ts +2 -0
- package/src/components/atomic-crm/providers/fakerest/internal/listParser.ts +48 -0
- package/src/components/atomic-crm/providers/fakerest/internal/supabaseAdapter.spec.ts +721 -0
- package/src/components/atomic-crm/providers/fakerest/internal/supabaseAdapter.ts +49 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformContainsFilter.spec.ts +35 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformContainsFilter.ts +17 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformFilter.ts +57 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformInFilter.spec.ts +32 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformInFilter.ts +17 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformOrFilter.spec.ts +23 -0
- package/src/components/atomic-crm/providers/fakerest/internal/transformOrFilter.ts +17 -0
- package/src/components/atomic-crm/providers/supabase/authProvider.ts +121 -0
- package/src/components/atomic-crm/providers/supabase/dataProvider.ts +407 -0
- package/src/components/atomic-crm/providers/supabase/index.ts +2 -0
- package/src/components/atomic-crm/providers/supabase/supabase.ts +34 -0
- package/src/components/atomic-crm/providers/types.ts +1 -0
- package/src/components/atomic-crm/root/CRM.tsx +167 -0
- package/src/components/atomic-crm/root/ConfigurationContext.tsx +80 -0
- package/src/components/atomic-crm/root/defaultConfiguration.ts +64 -0
- package/src/components/atomic-crm/root/i18nProvider.tsx +25 -0
- package/src/components/atomic-crm/sales/SaleName.tsx +13 -0
- package/src/components/atomic-crm/sales/SalesCreate.tsx +51 -0
- package/src/components/atomic-crm/sales/SalesEdit.tsx +82 -0
- package/src/components/atomic-crm/sales/SalesInputs.tsx +31 -0
- package/src/components/atomic-crm/sales/SalesList.tsx +62 -0
- package/src/components/atomic-crm/sales/index.ts +12 -0
- package/src/components/atomic-crm/settings/DatabaseSettings.tsx +169 -0
- package/src/components/atomic-crm/settings/SettingsPage.tsx +259 -0
- package/src/components/atomic-crm/setup/SupabaseSetupWizard.tsx +215 -0
- package/src/components/atomic-crm/simple-list/ListNoResults.tsx +53 -0
- package/src/components/atomic-crm/simple-list/ListPlaceholder.tsx +9 -0
- package/src/components/atomic-crm/simple-list/SimpleList.tsx +245 -0
- package/src/components/atomic-crm/simple-list/SimpleListItem.tsx +138 -0
- package/src/components/atomic-crm/simple-list/SimpleListLoading.tsx +60 -0
- package/src/components/atomic-crm/tags/RoundButton.tsx +10 -0
- package/src/components/atomic-crm/tags/TagChip.tsx +45 -0
- package/src/components/atomic-crm/tags/TagCreateModal.tsx +39 -0
- package/src/components/atomic-crm/tags/TagDialog.tsx +118 -0
- package/src/components/atomic-crm/tags/TagEditModal.tsx +42 -0
- package/src/components/atomic-crm/tags/colors.ts +12 -0
- package/src/components/atomic-crm/tasks/AddTask.tsx +191 -0
- package/src/components/atomic-crm/tasks/Task.tsx +184 -0
- package/src/components/atomic-crm/tasks/TaskEdit.tsx +96 -0
- package/src/components/atomic-crm/tasks/TasksIterator.tsx +30 -0
- package/src/components/atomic-crm/types.ts +226 -0
- package/src/components/supabase/forgot-password-page.tsx +86 -0
- package/src/components/supabase/layout.tsx +27 -0
- package/src/components/supabase/set-password-page.tsx +119 -0
- package/src/components/ui/README.md +34 -0
- package/src/components/ui/accordion.tsx +64 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/avatar.tsx +99 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/command.tsx +175 -0
- package/src/components/ui/dialog.tsx +133 -0
- package/src/components/ui/drawer.tsx +133 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/progress.tsx +29 -0
- package/src/components/ui/radio-group.tsx +43 -0
- package/src/components/ui/select.tsx +183 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/sidebar.tsx +724 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +38 -0
- package/src/components/ui/spinner.tsx +51 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/hooks/saved-queries.tsx +67 -0
- package/src/hooks/simple-form-iterator-context.tsx +70 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/useBulkExport.tsx +61 -0
- package/src/hooks/useSupportCreateSuggestion.tsx +188 -0
- package/src/hooks/user-menu-context.tsx +24 -0
- package/src/index.css +170 -0
- package/src/lib/field.type.ts +22 -0
- package/src/lib/genericMemo.ts +18 -0
- package/src/lib/i18nProvider.ts +9 -0
- package/src/lib/sanitizeInputRestProps.ts +46 -0
- package/src/lib/supabase-config.ts +123 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/setupTests.js +5 -0
- package/src/vite-env.d.ts +1 -0
- package/supabase/config.toml +157 -0
- package/supabase/functions/.env.development +7 -0
- package/supabase/functions/_shared/db.ts +187 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +13 -0
- package/supabase/functions/_shared/utils.ts +13 -0
- package/supabase/functions/mergeContacts/index.ts +215 -0
- package/supabase/functions/postmark/addNoteToContact.ts +129 -0
- package/supabase/functions/postmark/extractMailContactData.ts +41 -0
- package/supabase/functions/postmark/getExpectedAuthorization.ts +4 -0
- package/supabase/functions/postmark/getNoteContent.ts +6 -0
- package/supabase/functions/postmark/index.ts +210 -0
- package/supabase/functions/updatePassword/index.ts +50 -0
- package/supabase/functions/users/index.ts +206 -0
- package/supabase/migrations/20240730075029_init_db.sql +600 -0
- package/supabase/migrations/20240730075425_init_triggers.sql +57 -0
- package/supabase/migrations/20240806124555_task_sales_id.sql +1 -0
- package/supabase/migrations/20240807082449_remove-aquisition.sql +20 -0
- package/supabase/migrations/20240808141826_init_state_configure.sql +9 -0
- package/supabase/migrations/20240813084010_tags_policy.sql +18 -0
- package/supabase/migrations/20241104153231_sales_policies.sql +7 -0
- package/supabase/migrations/20250109152531_email_jsonb.sql +43 -0
- package/supabase/migrations/20250113132531_phone_jsonb.sql +67 -0
- package/supabase/migrations/20251204172855_merge_contacts_function.sql +153 -0
- package/supabase/migrations/20251204201317_drop_merge_contacts_function.sql +2 -0
- package/supabase/seed.sql +0 -0
- package/supabase/templates/invite.html +70 -0
- package/supabase/templates/recovery.html +75 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { InputProps } from "ra-core";
|
|
3
|
+
import { useInput, FieldTitle, useEvent, useResourceContext } from "ra-core";
|
|
4
|
+
import {
|
|
5
|
+
FormControl,
|
|
6
|
+
FormError,
|
|
7
|
+
FormField,
|
|
8
|
+
FormLabel,
|
|
9
|
+
} from "@/components/admin/form";
|
|
10
|
+
import { Input } from "@/components/ui/input";
|
|
11
|
+
import { InputHelperText } from "@/components/admin/input-helper-text";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Date picker input for editing date values in "YYYY-MM-DD" format.
|
|
16
|
+
*
|
|
17
|
+
* Use `<DateInput>` for publication dates, deadlines, birthdays, or any date field. Renders
|
|
18
|
+
* a native browser date picker (appearance varies by browser). Automatically handles date parsing
|
|
19
|
+
* from ISO strings, Date objects, or timestamps.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link https://marmelab.com/shadcn-admin-kit/docs/dateinput/ DateInput documentation}
|
|
22
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date MDN documentation for input type="date"}
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* import {
|
|
26
|
+
* Edit,
|
|
27
|
+
* SimpleForm,
|
|
28
|
+
* DateInput,
|
|
29
|
+
* TextInput,
|
|
30
|
+
* } from '@/components/admin';
|
|
31
|
+
*
|
|
32
|
+
* const PostEdit = () => (
|
|
33
|
+
* <Edit>
|
|
34
|
+
* <SimpleForm>
|
|
35
|
+
* <TextInput source="title" />
|
|
36
|
+
* <DateInput source="published_at" />
|
|
37
|
+
* <DateInput source="expires_at" />
|
|
38
|
+
* </SimpleForm>
|
|
39
|
+
* </Edit>
|
|
40
|
+
* );
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // If the initial value string contains more than a date (e.g. an hour, a timezone),
|
|
44
|
+
* // these details are ignored.
|
|
45
|
+
* <DateInput source="published_at" defaultValue="2021-09-11T20:46:20.000-04:00" />
|
|
46
|
+
* // The input will display '2021-09-11' regardless of the browser timezone.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // If the initial value is a Date object, DateInput converts it to a string
|
|
50
|
+
* // and ignores the timezone.
|
|
51
|
+
* <DateInput source="published_at" defaultValue={new Date("2021-09-11T20:46:20.000-04:00")} />
|
|
52
|
+
* // The input will display '2021-09-11' regardless of the browser timezone.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // If you want to manipulate the value from the field to adjust its timezone, use the format prop
|
|
56
|
+
* <DateInput source="published_at" format={value => new Date(value).toISOString().split("T")[0]} />
|
|
57
|
+
* // The input will display the UTC day regardless of the browser timezone.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // If you want the returned value to be a Date, you must pass a custom parse method
|
|
61
|
+
* // to convert the form value (which is always a date string) back to a Date object.
|
|
62
|
+
* <DateInput source="published_at" parse={val => new Date(val)} />
|
|
63
|
+
*/
|
|
64
|
+
export const DateInput = (props: DateInputProps) => {
|
|
65
|
+
const {
|
|
66
|
+
className,
|
|
67
|
+
inputClassName,
|
|
68
|
+
defaultValue,
|
|
69
|
+
format = defaultFormat,
|
|
70
|
+
label,
|
|
71
|
+
source,
|
|
72
|
+
helperText,
|
|
73
|
+
onChange,
|
|
74
|
+
onFocus,
|
|
75
|
+
validate,
|
|
76
|
+
disabled,
|
|
77
|
+
readOnly,
|
|
78
|
+
...rest
|
|
79
|
+
} = props;
|
|
80
|
+
|
|
81
|
+
const resource = useResourceContext(props);
|
|
82
|
+
|
|
83
|
+
const {
|
|
84
|
+
field,
|
|
85
|
+
fieldState: _fieldState,
|
|
86
|
+
id,
|
|
87
|
+
isRequired,
|
|
88
|
+
} = useInput({
|
|
89
|
+
defaultValue,
|
|
90
|
+
source,
|
|
91
|
+
validate,
|
|
92
|
+
disabled,
|
|
93
|
+
readOnly,
|
|
94
|
+
format,
|
|
95
|
+
...rest,
|
|
96
|
+
});
|
|
97
|
+
const localInputRef = React.useRef<HTMLInputElement>(null);
|
|
98
|
+
// DateInput is not a really controlled input to ensure users can start entering a date, go to another input and come back to complete it.
|
|
99
|
+
// This ref stores the value that is passed to the input defaultValue prop to solve this issue.
|
|
100
|
+
const initialDefaultValueRef = React.useRef(field.value);
|
|
101
|
+
// As the defaultValue prop won't trigger a remount of the HTML input, we will force it by changing the key.
|
|
102
|
+
const [inputKey, setInputKey] = React.useState(1);
|
|
103
|
+
// This ref let us track that the last change of the form state value was made by the input itself
|
|
104
|
+
const wasLastChangedByInput = React.useRef(false);
|
|
105
|
+
|
|
106
|
+
// This effect ensures we stays in sync with the react-hook-form state when the value changes from outside the input
|
|
107
|
+
// for instance by using react-hook-form reset or setValue methods.
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
// Ignore react-hook-form state changes if it came from the input itself
|
|
110
|
+
if (wasLastChangedByInput.current) {
|
|
111
|
+
// Resets the flag to ensure futures changes are handled
|
|
112
|
+
wasLastChangedByInput.current = false;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const hasNewValueFromForm =
|
|
117
|
+
localInputRef.current?.value !== field.value &&
|
|
118
|
+
!(localInputRef.current?.value === "" && field.value == null);
|
|
119
|
+
|
|
120
|
+
if (hasNewValueFromForm) {
|
|
121
|
+
// The value has changed from outside the input, we update the input value
|
|
122
|
+
initialDefaultValueRef.current = field.value;
|
|
123
|
+
// Trigger a remount of the HTML input
|
|
124
|
+
setInputKey((r) => r + 1);
|
|
125
|
+
// Resets the flag to ensure futures changes are handled
|
|
126
|
+
wasLastChangedByInput.current = false;
|
|
127
|
+
}
|
|
128
|
+
}, [setInputKey, field.value]);
|
|
129
|
+
|
|
130
|
+
const { onBlur: onBlurFromField } = field;
|
|
131
|
+
const hasFocus = React.useRef(false);
|
|
132
|
+
|
|
133
|
+
// Update the input text when the user types in the input.
|
|
134
|
+
// Also, update the react-hook-form value if the input value is a valid date string.
|
|
135
|
+
const handleChange = useEvent(
|
|
136
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
137
|
+
if (onChange) {
|
|
138
|
+
onChange(event);
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
typeof event.target === "undefined" ||
|
|
142
|
+
typeof event.target.value === "undefined"
|
|
143
|
+
) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const target = event.target;
|
|
147
|
+
const newValue = target.value;
|
|
148
|
+
const isNewValueValid =
|
|
149
|
+
newValue === "" ||
|
|
150
|
+
(target.valueAsDate != null &&
|
|
151
|
+
!isNaN(new Date(target.valueAsDate).getTime()));
|
|
152
|
+
|
|
153
|
+
// Some browsers will return null for an invalid date
|
|
154
|
+
// so we only change react-hook-form value if it's not null.
|
|
155
|
+
// The input reset is handled in the onBlur event handler
|
|
156
|
+
if (newValue !== "" && newValue != null && isNewValueValid) {
|
|
157
|
+
field.onChange(newValue);
|
|
158
|
+
// Track the fact that the next react-hook-form state change was triggered by the input itself
|
|
159
|
+
wasLastChangedByInput.current = true;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const handleFocus = useEvent((event: React.FocusEvent<HTMLInputElement>) => {
|
|
165
|
+
if (onFocus) {
|
|
166
|
+
onFocus(event);
|
|
167
|
+
}
|
|
168
|
+
hasFocus.current = true;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const handleBlur = useEvent(() => {
|
|
172
|
+
hasFocus.current = false;
|
|
173
|
+
|
|
174
|
+
if (!localInputRef.current) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const newValue = localInputRef.current.value;
|
|
179
|
+
// To ensure users can clear the input, we check its value on blur
|
|
180
|
+
// and submit it to react-hook-form
|
|
181
|
+
const isNewValueValid =
|
|
182
|
+
newValue === "" ||
|
|
183
|
+
(localInputRef.current.valueAsDate != null &&
|
|
184
|
+
!isNaN(new Date(localInputRef.current.valueAsDate).getTime()));
|
|
185
|
+
|
|
186
|
+
if (isNewValueValid && field.value !== newValue) {
|
|
187
|
+
field.onChange(newValue ?? "");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (onBlurFromField) {
|
|
191
|
+
onBlurFromField();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const { ref, name } = field;
|
|
196
|
+
const inputRef = React.useCallback(
|
|
197
|
+
(node: HTMLInputElement | null) => {
|
|
198
|
+
if (typeof ref === "function") {
|
|
199
|
+
ref(node);
|
|
200
|
+
} else if (ref && node) {
|
|
201
|
+
(ref as React.MutableRefObject<HTMLInputElement | null>).current = node;
|
|
202
|
+
}
|
|
203
|
+
localInputRef.current = node;
|
|
204
|
+
},
|
|
205
|
+
[ref],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<FormField id={id} className={className} name={name}>
|
|
210
|
+
{label !== false && (
|
|
211
|
+
<FormLabel>
|
|
212
|
+
<FieldTitle
|
|
213
|
+
label={label}
|
|
214
|
+
source={source}
|
|
215
|
+
resource={resource}
|
|
216
|
+
isRequired={isRequired}
|
|
217
|
+
/>
|
|
218
|
+
</FormLabel>
|
|
219
|
+
)}
|
|
220
|
+
<FormControl>
|
|
221
|
+
<Input
|
|
222
|
+
ref={inputRef}
|
|
223
|
+
defaultValue={format(initialDefaultValueRef.current) ?? ""}
|
|
224
|
+
key={inputKey}
|
|
225
|
+
type="date"
|
|
226
|
+
onChange={handleChange}
|
|
227
|
+
onFocus={handleFocus}
|
|
228
|
+
onBlur={handleBlur}
|
|
229
|
+
className={cn(
|
|
230
|
+
"ra-input",
|
|
231
|
+
`ra-input-${source}`,
|
|
232
|
+
"[color-scheme:light] dark:[color-scheme:dark] relative [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-3 [&::-webkit-calendar-picker-indicator]:opacity-100",
|
|
233
|
+
inputClassName,
|
|
234
|
+
)}
|
|
235
|
+
disabled={disabled || readOnly}
|
|
236
|
+
readOnly={readOnly}
|
|
237
|
+
{...rest}
|
|
238
|
+
/>
|
|
239
|
+
</FormControl>
|
|
240
|
+
<InputHelperText helperText={helperText} />
|
|
241
|
+
<FormError />
|
|
242
|
+
</FormField>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export type DateInputProps = InputProps & {
|
|
247
|
+
inputClassName?: string;
|
|
248
|
+
} & Omit<React.ComponentProps<"input">, "label" | "defaultValue">;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Convert Date object to String, using the local timezone
|
|
252
|
+
*
|
|
253
|
+
* @param {Date} value value to convert
|
|
254
|
+
* @returns {String} A standardized date (yyyy-MM-dd), to be passed to an <input type="date" />
|
|
255
|
+
*/
|
|
256
|
+
const convertDateToString = (value: Date) => {
|
|
257
|
+
if (!(value instanceof Date) || isNaN(value.getDate())) return "";
|
|
258
|
+
const localDate = new Date(value.getTime());
|
|
259
|
+
const pad = "00";
|
|
260
|
+
const yyyy = localDate.getFullYear().toString();
|
|
261
|
+
const MM = (localDate.getMonth() + 1).toString();
|
|
262
|
+
const dd = localDate.getDate().toString();
|
|
263
|
+
return `${yyyy}-${(pad + MM).slice(-2)}-${(pad + dd).slice(-2)}`;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Convert a form state value to a date string for the `<input type="date">` value.
|
|
270
|
+
*
|
|
271
|
+
* Form state values can be anything from:
|
|
272
|
+
* - a string in the "YYYY-MM-DD" format
|
|
273
|
+
* - A valid date string
|
|
274
|
+
* - an ISO date string
|
|
275
|
+
* - a Date object
|
|
276
|
+
* - a Linux timestamp
|
|
277
|
+
* - an empty string
|
|
278
|
+
*
|
|
279
|
+
* When it's not a bare date string (YYYY-MM-DD), the value is converted to
|
|
280
|
+
* this format using the JS Date object.
|
|
281
|
+
* THIS MAY CHANGE THE DATE VALUE depending on the browser locale.
|
|
282
|
+
* For example, the string "09/11/2021" may be converted to "2021-09-10"
|
|
283
|
+
* in Honolulu. This is expected behavior.
|
|
284
|
+
* If this is not what you want, you should provide your own parse method.
|
|
285
|
+
*
|
|
286
|
+
* The output is always a string in the "YYYY-MM-DD" format.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* defaultFormat('2021-09-11'); // '2021-09-11'
|
|
290
|
+
* defaultFormat('09/11/2021'); // '2021-09-11' (may change depending on the browser locale)
|
|
291
|
+
* defaultFormat('2021-09-11T20:46:20.000Z'); // '2021-09-11' (may change depending on the browser locale)
|
|
292
|
+
* defaultFormat(new Date('2021-09-11T20:46:20.000Z')); // '2021-09-11' (may change depending on the browser locale)
|
|
293
|
+
* defaultFormat(1631385980000); // '2021-09-11' (may change depending on the browser locale)
|
|
294
|
+
* defaultFormat(''); // null
|
|
295
|
+
*/
|
|
296
|
+
const defaultFormat = (value: string | Date | number) => {
|
|
297
|
+
// null, undefined and empty string values should not go through dateFormatter
|
|
298
|
+
// otherwise, it returns undefined and will make the input an uncontrolled one.
|
|
299
|
+
if (value == null || value === "") {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Date objects should be converted to strings
|
|
304
|
+
if (value instanceof Date) {
|
|
305
|
+
return convertDateToString(value);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Valid date strings (YYYY-MM-DD) should be considered as is
|
|
309
|
+
if (typeof value === "string") {
|
|
310
|
+
if (dateRegex.test(value)) {
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// other values (e.g., localized date strings, timestamps) need to be converted to Dates first
|
|
316
|
+
return convertDateToString(new Date(value));
|
|
317
|
+
};
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import type { InputProps } from "ra-core";
|
|
4
|
+
import { useInput, FieldTitle } from "ra-core";
|
|
5
|
+
import {
|
|
6
|
+
FormControl,
|
|
7
|
+
FormError,
|
|
8
|
+
FormField,
|
|
9
|
+
FormLabel,
|
|
10
|
+
} from "@/components/admin/form";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import { InputHelperText } from "@/components/admin/input-helper-text";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Date and time picker input for editing datetime values with timezone support.
|
|
16
|
+
*
|
|
17
|
+
* Use `<DateTimeInput>` for timestamps like "created at", "updated at", or scheduled events.
|
|
18
|
+
* Renders a native browser datetime-local picker. Expects and returns ISO 8601 formatted strings
|
|
19
|
+
* (e.g. '2025-11-17T10:10:32.390Z'), automatically converting other formats like Date objects or timestamps.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link https://marmelab.com/shadcn-admin-kit/docs/datetimeinput/ DateTimeInput documentation}
|
|
22
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local MDN documentation for input type="datetime-local"}
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* import {
|
|
26
|
+
* Edit,
|
|
27
|
+
* SimpleForm,
|
|
28
|
+
* DateTimeInput,
|
|
29
|
+
* TextInput,
|
|
30
|
+
* } from '@/components/admin';
|
|
31
|
+
*
|
|
32
|
+
* const EventEdit = () => (
|
|
33
|
+
* <Edit>
|
|
34
|
+
* <SimpleForm>
|
|
35
|
+
* <TextInput source="title" />
|
|
36
|
+
* <DateTimeInput source="starts_at" />
|
|
37
|
+
* <DateTimeInput source="ends_at" />
|
|
38
|
+
* </SimpleForm>
|
|
39
|
+
* </Edit>
|
|
40
|
+
* );
|
|
41
|
+
*/
|
|
42
|
+
export const DateTimeInput = ({
|
|
43
|
+
className,
|
|
44
|
+
defaultValue,
|
|
45
|
+
format = formatDateTime,
|
|
46
|
+
parse = convertDateStringToISO,
|
|
47
|
+
label,
|
|
48
|
+
helperText,
|
|
49
|
+
onBlur,
|
|
50
|
+
onChange,
|
|
51
|
+
onFocus,
|
|
52
|
+
source,
|
|
53
|
+
resource,
|
|
54
|
+
validate,
|
|
55
|
+
disabled,
|
|
56
|
+
readOnly,
|
|
57
|
+
...rest
|
|
58
|
+
}: DateTimeInputProps) => {
|
|
59
|
+
const { field, id, isRequired } = useInput({
|
|
60
|
+
defaultValue,
|
|
61
|
+
onBlur,
|
|
62
|
+
resource,
|
|
63
|
+
source,
|
|
64
|
+
validate,
|
|
65
|
+
disabled,
|
|
66
|
+
readOnly,
|
|
67
|
+
format,
|
|
68
|
+
parse,
|
|
69
|
+
...rest,
|
|
70
|
+
});
|
|
71
|
+
const localInputRef = React.useRef<HTMLInputElement>(undefined);
|
|
72
|
+
// DateInput is not a really controlled input to ensure users can start entering a date, go to another input and come back to complete it.
|
|
73
|
+
// This ref stores the value that is passed to the input defaultValue prop to solve this issue.
|
|
74
|
+
const initialDefaultValueRef = React.useRef(field.value);
|
|
75
|
+
// As the defaultValue prop won't trigger a remount of the HTML input, we will force it by changing the key.
|
|
76
|
+
const [inputKey, setInputKey] = React.useState(1);
|
|
77
|
+
// This ref let us track that the last change of the form state value was made by the input itself
|
|
78
|
+
const wasLastChangedByInput = React.useRef(false);
|
|
79
|
+
|
|
80
|
+
// This effect ensures we stays in sync with the react-hook-form state when the value changes from outside the input
|
|
81
|
+
// for instance by using react-hook-form reset or setValue methods.
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
// Ignore react-hook-form state changes if it came from the input itself
|
|
84
|
+
if (wasLastChangedByInput.current) {
|
|
85
|
+
// Resets the flag to ensure futures changes are handled
|
|
86
|
+
wasLastChangedByInput.current = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hasNewValueFromForm =
|
|
91
|
+
localInputRef.current?.value !== field.value &&
|
|
92
|
+
!(localInputRef.current?.value === "" && field.value == null);
|
|
93
|
+
|
|
94
|
+
if (hasNewValueFromForm) {
|
|
95
|
+
// The value has changed from outside the input, we update the input value
|
|
96
|
+
initialDefaultValueRef.current = field.value;
|
|
97
|
+
// Trigger a remount of the HTML input
|
|
98
|
+
setInputKey((r) => r + 1);
|
|
99
|
+
// Resets the flag to ensure futures changes are handled
|
|
100
|
+
wasLastChangedByInput.current = false;
|
|
101
|
+
}
|
|
102
|
+
}, [setInputKey, field.value]);
|
|
103
|
+
|
|
104
|
+
const { onBlur: onBlurFromField } = field;
|
|
105
|
+
const hasFocus = React.useRef(false);
|
|
106
|
+
|
|
107
|
+
// update the input text when the user types in the input
|
|
108
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
109
|
+
if (onChange) {
|
|
110
|
+
onChange(event);
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
typeof event.target === "undefined" ||
|
|
114
|
+
typeof event.target.value === "undefined"
|
|
115
|
+
) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const target = event.target;
|
|
119
|
+
const newValue = target.value;
|
|
120
|
+
const isNewValueValid =
|
|
121
|
+
newValue === "" || !isNaN(new Date(target.value).getTime());
|
|
122
|
+
|
|
123
|
+
// Some browsers will return null for an invalid date
|
|
124
|
+
// so we only change react-hook-form value if it's not null.
|
|
125
|
+
// The input reset is handled in the onBlur event handler
|
|
126
|
+
if (newValue !== "" && newValue != null && isNewValueValid) {
|
|
127
|
+
field.onChange(newValue);
|
|
128
|
+
// Track the fact that the next react-hook-form state change was triggered by the input itself
|
|
129
|
+
wasLastChangedByInput.current = true;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
|
134
|
+
if (onFocus) {
|
|
135
|
+
onFocus(event);
|
|
136
|
+
}
|
|
137
|
+
hasFocus.current = true;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleBlur = () => {
|
|
141
|
+
hasFocus.current = false;
|
|
142
|
+
|
|
143
|
+
if (!localInputRef.current) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const newValue = localInputRef.current.value;
|
|
148
|
+
// To ensure users can clear the input, we check its value on blur
|
|
149
|
+
// and submit it to react-hook-form
|
|
150
|
+
const isNewValueValid =
|
|
151
|
+
newValue === "" ||
|
|
152
|
+
!isNaN(new Date(localInputRef.current.value).getTime());
|
|
153
|
+
|
|
154
|
+
if (isNewValueValid && field.value !== newValue) {
|
|
155
|
+
field.onChange(newValue ?? "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (onBlurFromField) {
|
|
159
|
+
onBlurFromField();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const { ref, name } = field;
|
|
164
|
+
const inputRef = useForkRef(ref, localInputRef);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<FormField id={id} className={className} name={field.name}>
|
|
168
|
+
{label !== false && (
|
|
169
|
+
<FormLabel>
|
|
170
|
+
<FieldTitle
|
|
171
|
+
label={label}
|
|
172
|
+
source={source}
|
|
173
|
+
resource={resource}
|
|
174
|
+
isRequired={isRequired}
|
|
175
|
+
/>
|
|
176
|
+
</FormLabel>
|
|
177
|
+
)}
|
|
178
|
+
<FormControl>
|
|
179
|
+
<Input
|
|
180
|
+
id={id}
|
|
181
|
+
ref={inputRef}
|
|
182
|
+
name={name}
|
|
183
|
+
defaultValue={format(initialDefaultValueRef.current)}
|
|
184
|
+
key={inputKey}
|
|
185
|
+
type="datetime-local"
|
|
186
|
+
onChange={handleChange}
|
|
187
|
+
onFocus={handleFocus}
|
|
188
|
+
onBlur={handleBlur}
|
|
189
|
+
className={clsx(
|
|
190
|
+
"ra-input",
|
|
191
|
+
`ra-input-${source}`,
|
|
192
|
+
"[color-scheme:light] dark:[color-scheme:dark] relative [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:absolute [&::-webkit-calendar-picker-indicator]:right-3 [&::-webkit-calendar-picker-indicator]:opacity-100",
|
|
193
|
+
className,
|
|
194
|
+
)}
|
|
195
|
+
disabled={disabled || readOnly}
|
|
196
|
+
readOnly={readOnly}
|
|
197
|
+
/>
|
|
198
|
+
</FormControl>
|
|
199
|
+
<InputHelperText helperText={helperText} />
|
|
200
|
+
<FormError />
|
|
201
|
+
</FormField>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export type DateTimeInputProps = Omit<InputProps, "defaultValue"> & {
|
|
206
|
+
defaultValue?: string | number | Date;
|
|
207
|
+
} & Omit<React.ComponentProps<"input">, "defaultValue" | "type">;
|
|
208
|
+
|
|
209
|
+
const leftPad =
|
|
210
|
+
(nb = 2) =>
|
|
211
|
+
(value: number) =>
|
|
212
|
+
("0".repeat(nb) + value).slice(-nb);
|
|
213
|
+
const leftPad4 = leftPad(4);
|
|
214
|
+
const leftPad2 = leftPad(2);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {Date} value value to convert
|
|
218
|
+
* @returns {String} A standardized datetime (yyyy-MM-ddThh:mm), to be passed to an <input type="datetime-local" />
|
|
219
|
+
*/
|
|
220
|
+
const convertDateToString = (value: Date) => {
|
|
221
|
+
if (!(value instanceof Date) || isNaN(value.getDate())) return "";
|
|
222
|
+
const yyyy = leftPad4(value.getFullYear());
|
|
223
|
+
const MM = leftPad2(value.getMonth() + 1);
|
|
224
|
+
const dd = leftPad2(value.getDate());
|
|
225
|
+
const hh = leftPad2(value.getHours());
|
|
226
|
+
const mm = leftPad2(value.getMinutes());
|
|
227
|
+
return `${yyyy}-${MM}-${dd}T${hh}:${mm}`;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// yyyy-MM-ddThh:mm
|
|
231
|
+
const dateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Converts a date from the dataProvider, with timezone, to a date string
|
|
235
|
+
* without timezone for use in an <input type="datetime-local" />.
|
|
236
|
+
*
|
|
237
|
+
* @param {Date | String} value date string or object
|
|
238
|
+
*/
|
|
239
|
+
const formatDateTime = (value: string | Date) => {
|
|
240
|
+
// null, undefined and empty string values should not go through convertDateToString
|
|
241
|
+
// otherwise, it returns undefined and will make the input an uncontrolled one.
|
|
242
|
+
if (value == null || value === "") {
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (value instanceof Date) {
|
|
247
|
+
return convertDateToString(value);
|
|
248
|
+
}
|
|
249
|
+
// valid dates should not be converted
|
|
250
|
+
if (dateTimeRegex.test(value)) {
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return convertDateToString(new Date(value));
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// converts a date string entered usinf a datetime-local input
|
|
258
|
+
// into an ISO date using the browser timezone
|
|
259
|
+
const convertDateStringToISO = (date: string) => {
|
|
260
|
+
const localDate = new Date(date);
|
|
261
|
+
return localDate.toISOString();
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Imported from material-ui
|
|
266
|
+
* Merges refs into a single memoized callback ref or `null`.
|
|
267
|
+
*
|
|
268
|
+
* ```tsx
|
|
269
|
+
* const rootRef = React.useRef<Instance>(null);
|
|
270
|
+
* const refFork = useForkRef(rootRef, props.ref);
|
|
271
|
+
*
|
|
272
|
+
* return (
|
|
273
|
+
* <Root {...props} ref={refFork} />
|
|
274
|
+
* );
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
277
|
+
* @param {Array<React.Ref<Instance> | undefined>} refs The ref array.
|
|
278
|
+
* @returns {React.RefCallback<Instance> | null} The new ref callback.
|
|
279
|
+
*/
|
|
280
|
+
function useForkRef<Instance>(
|
|
281
|
+
...refs: Array<React.Ref<Instance> | undefined>
|
|
282
|
+
): React.RefCallback<Instance> | null {
|
|
283
|
+
const cleanupRef = React.useRef<() => void>(undefined);
|
|
284
|
+
|
|
285
|
+
const refEffect = React.useCallback((instance: Instance) => {
|
|
286
|
+
const cleanups = refs.map((ref) => {
|
|
287
|
+
if (ref == null) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (typeof ref === "function") {
|
|
292
|
+
const refCallback = ref;
|
|
293
|
+
const refCleanup: void | (() => void) = refCallback(instance);
|
|
294
|
+
return typeof refCleanup === "function"
|
|
295
|
+
? refCleanup
|
|
296
|
+
: () => {
|
|
297
|
+
refCallback(null);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ref.current = instance;
|
|
302
|
+
return () => {
|
|
303
|
+
ref.current = null;
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return () => {
|
|
308
|
+
cleanups.forEach((refCleanup) => refCleanup?.());
|
|
309
|
+
};
|
|
310
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
311
|
+
}, refs);
|
|
312
|
+
|
|
313
|
+
return React.useMemo(() => {
|
|
314
|
+
if (refs.every((ref) => ref == null)) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return (value) => {
|
|
319
|
+
if (cleanupRef.current) {
|
|
320
|
+
cleanupRef.current();
|
|
321
|
+
cleanupRef.current = undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (value != null) {
|
|
325
|
+
cleanupRef.current = refEffect(value);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
// TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler -- intentionally ignoring that the dependency array must be an array literal
|
|
329
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
330
|
+
}, refs);
|
|
331
|
+
}
|