@spaceinvoices/react-ui 0.1.1
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 +21 -0
- package/README.md +340 -0
- package/cli/dist/index.js +922 -0
- package/package.json +87 -0
- package/registry.json +600 -0
- package/spaceinvoices.schema.json +47 -0
- package/src/app.tsx +25 -0
- package/src/common/autocomplete.tsx +135 -0
- package/src/components/activities/activity-timeline.tsx +160 -0
- package/src/components/activities/index.ts +1 -0
- package/src/components/activities/locales/de.ts +30 -0
- package/src/components/activities/locales/sl.ts +30 -0
- package/src/components/advance-invoices/advance-invoices.hooks.ts +75 -0
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +702 -0
- package/src/components/advance-invoices/create/locales/de.ts +29 -0
- package/src/components/advance-invoices/create/locales/sl.ts +25 -0
- package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +74 -0
- package/src/components/advance-invoices/index.ts +5 -0
- package/src/components/advance-invoices/list/index.ts +3 -0
- package/src/components/advance-invoices/list/list-row-actions.tsx +119 -0
- package/src/components/advance-invoices/list/list-table.tsx +178 -0
- package/src/components/advance-invoices/list/locales/de.ts +32 -0
- package/src/components/advance-invoices/list/locales/sl.ts +32 -0
- package/src/components/advance-invoices/list/use-advance-invoice-download.ts +63 -0
- package/src/components/button-loader.tsx +11 -0
- package/src/components/combobox.tsx +96 -0
- package/src/components/company-registry/company-registry-autocomplete.tsx +151 -0
- package/src/components/company-registry/company-registry.hooks.ts +67 -0
- package/src/components/company-registry/index.ts +7 -0
- package/src/components/credit-notes/create/create-credit-note-form.tsx +332 -0
- package/src/components/credit-notes/create/index.ts +1 -0
- package/src/components/credit-notes/create/locales/de.ts +69 -0
- package/src/components/credit-notes/create/locales/sl.ts +67 -0
- package/src/components/credit-notes/credit-notes.hooks.ts +22 -0
- package/src/components/credit-notes/index.ts +10 -0
- package/src/components/credit-notes/list/index.ts +3 -0
- package/src/components/credit-notes/list/list-row-actions.tsx +116 -0
- package/src/components/credit-notes/list/list-table.tsx +183 -0
- package/src/components/credit-notes/list/locales/de.ts +33 -0
- package/src/components/credit-notes/list/locales/sl.ts +33 -0
- package/src/components/credit-notes/list/use-credit-note-download.ts +65 -0
- package/src/components/customers/create-customer-form/create-customer-form.tsx +134 -0
- package/src/components/customers/create-customer-form/locales/de.ts +20 -0
- package/src/components/customers/create-customer-form/locales/sl.ts +20 -0
- package/src/components/customers/customer-autocomplete.tsx +173 -0
- package/src/components/customers/customer-combobox.tsx +130 -0
- package/src/components/customers/customer-list-table/customer-list-row-actions.tsx +48 -0
- package/src/components/customers/customer-list-table/customer-list-table.tsx +124 -0
- package/src/components/customers/customer-list-table/index.ts +2 -0
- package/src/components/customers/customer-list-table/locales/de.ts +16 -0
- package/src/components/customers/customer-list-table/locales/sl.ts +16 -0
- package/src/components/customers/customers.hooks.test.ts +348 -0
- package/src/components/customers/customers.hooks.ts +57 -0
- package/src/components/customers/index.ts +5 -0
- package/src/components/dashboard/chart-empty-state.tsx +29 -0
- package/src/components/dashboard/collection-rate-card/collection-rate-card.tsx +80 -0
- package/src/components/dashboard/collection-rate-card/index.ts +4 -0
- package/src/components/dashboard/collection-rate-card/locales/sl.ts +3 -0
- package/src/components/dashboard/collection-rate-card/use-collection-rate.ts +74 -0
- package/src/components/dashboard/index.ts +54 -0
- package/src/components/dashboard/invoice-status-chart/index.ts +4 -0
- package/src/components/dashboard/invoice-status-chart/invoice-status-chart.tsx +130 -0
- package/src/components/dashboard/invoice-status-chart/locales/sl.ts +9 -0
- package/src/components/dashboard/invoice-status-chart/use-invoice-status.ts +105 -0
- package/src/components/dashboard/loading-card.tsx +19 -0
- package/src/components/dashboard/payment-methods-chart/index.ts +4 -0
- package/src/components/dashboard/payment-methods-chart/locales/sl.ts +12 -0
- package/src/components/dashboard/payment-methods-chart/payment-methods-chart.tsx +152 -0
- package/src/components/dashboard/payment-methods-chart/use-payment-methods.ts +50 -0
- package/src/components/dashboard/payment-trend-chart/index.ts +4 -0
- package/src/components/dashboard/payment-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/payment-trend-chart/payment-trend-chart.tsx +137 -0
- package/src/components/dashboard/payment-trend-chart/use-payment-trend.ts +92 -0
- package/src/components/dashboard/revenue-card.tsx +49 -0
- package/src/components/dashboard/revenue-trend-chart/index.ts +4 -0
- package/src/components/dashboard/revenue-trend-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/revenue-trend-chart/revenue-trend-chart.tsx +137 -0
- package/src/components/dashboard/revenue-trend-chart/use-revenue-trend.ts +93 -0
- package/src/components/dashboard/shared/index.ts +5 -0
- package/src/components/dashboard/shared/use-revenue-data.ts +160 -0
- package/src/components/dashboard/shared/use-stats-counts.ts +89 -0
- package/src/components/dashboard/shared/use-stats-query.ts +38 -0
- package/src/components/dashboard/stat-card.tsx +41 -0
- package/src/components/dashboard/tax-collected-card/index.ts +2 -0
- package/src/components/dashboard/tax-collected-card/tax-collected-card.tsx +77 -0
- package/src/components/dashboard/tax-collected-card/use-tax-collected.ts +145 -0
- package/src/components/dashboard/top-customers-chart/index.ts +4 -0
- package/src/components/dashboard/top-customers-chart/locales/sl.ts +5 -0
- package/src/components/dashboard/top-customers-chart/top-customers-chart.tsx +130 -0
- package/src/components/dashboard/top-customers-chart/use-top-customers.ts +72 -0
- package/src/components/documents/create/document-add-item-form.tsx +379 -0
- package/src/components/documents/create/document-add-item-tax-rate-field.tsx +120 -0
- package/src/components/documents/create/document-details-section.tsx +597 -0
- package/src/components/documents/create/document-items-section.tsx +133 -0
- package/src/components/documents/create/document-recipient-section.tsx +101 -0
- package/src/components/documents/create/form-types.ts +36 -0
- package/src/components/documents/create/index.ts +9 -0
- package/src/components/documents/create/live-preview.tsx +235 -0
- package/src/components/documents/create/mark-as-paid-section.tsx +82 -0
- package/src/components/documents/create/prepare-document-submission.test.ts +132 -0
- package/src/components/documents/create/prepare-document-submission.ts +187 -0
- package/src/components/documents/create/prepare-preview-data.test.ts +155 -0
- package/src/components/documents/create/prepare-preview-data.ts +16 -0
- package/src/components/documents/create/smart-code-insert-button.tsx +139 -0
- package/src/components/documents/create/use-document-customer-form.ts +161 -0
- package/src/components/documents/document-preview.tsx +13 -0
- package/src/components/documents/documents.hooks.ts +146 -0
- package/src/components/documents/index.ts +23 -0
- package/src/components/documents/shared/document-preview-display.tsx +172 -0
- package/src/components/documents/shared/index.ts +3 -0
- package/src/components/documents/shared/scaled-document-preview.tsx +70 -0
- package/src/components/documents/shared/use-a4-scaling.ts +62 -0
- package/src/components/documents/types.ts +61 -0
- package/src/components/documents/view/document-actions-bar.tsx +328 -0
- package/src/components/documents/view/document-details-card.tsx +179 -0
- package/src/components/documents/view/document-payments-list.tsx +256 -0
- package/src/components/documents/view/index.ts +4 -0
- package/src/components/documents/view/locales/de.ts +85 -0
- package/src/components/documents/view/locales/sl.ts +84 -0
- package/src/components/documents/view/use-document-download.ts +125 -0
- package/src/components/entities/create-entity-form.tsx +105 -0
- package/src/components/entities/entities.hooks.ts +50 -0
- package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +103 -0
- package/src/components/entities/entity-settings-form/entity-settings-form.tsx +1326 -0
- package/src/components/entities/entity-settings-form/image-upload-with-crop.tsx +222 -0
- package/src/components/entities/entity-settings-form/index.ts +2 -0
- package/src/components/entities/entity-settings-form/input-with-preview.tsx +190 -0
- package/src/components/entities/entity-settings-form/locales/de.ts +192 -0
- package/src/components/entities/entity-settings-form/locales/sl.ts +188 -0
- package/src/components/entities/furs-settings-form/furs-settings-form.tsx +410 -0
- package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +320 -0
- package/src/components/entities/furs-settings-form/index.ts +3 -0
- package/src/components/entities/furs-settings-form/locales/de.ts +233 -0
- package/src/components/entities/furs-settings-form/locales/en.ts +194 -0
- package/src/components/entities/furs-settings-form/locales/sl.ts +196 -0
- package/src/components/entities/furs-settings-form/sections/certificate-settings-section.tsx +242 -0
- package/src/components/entities/furs-settings-form/sections/enable-fiscalization-section.tsx +139 -0
- package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +252 -0
- package/src/components/entities/furs-settings-form/sections/premises-management-section.tsx +370 -0
- package/src/components/entities/furs-settings-form/sections/register-premise-dialog.tsx +420 -0
- package/src/components/entities/keys.ts +2 -0
- package/src/components/entities/settings/branding-settings-form.tsx +274 -0
- package/src/components/entities/settings/company-settings-form.tsx +256 -0
- package/src/components/entities/settings/defaults-settings-form.tsx +501 -0
- package/src/components/entities/settings/email-settings-form.tsx +288 -0
- package/src/components/entities/settings/eslog-settings-form.tsx +113 -0
- package/src/components/entities/settings/index.ts +8 -0
- package/src/components/entities/settings/number-format-settings-form.tsx +244 -0
- package/src/components/entities/settings/pdf-template-selector/demo-invoice-data.ts +164 -0
- package/src/components/entities/settings/pdf-template-selector/index.ts +2 -0
- package/src/components/entities/settings/pdf-template-selector/locales/de.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/locales/sl.ts +18 -0
- package/src/components/entities/settings/pdf-template-selector/pdf-template-cards.tsx +49 -0
- package/src/components/entities/settings/settings-footer.tsx +16 -0
- package/src/components/entities/settings/tax-rules-settings-form.tsx +346 -0
- package/src/components/estimates/create/create-estimate-form.tsx +384 -0
- package/src/components/estimates/create/locales/de.ts +64 -0
- package/src/components/estimates/create/locales/sl.ts +63 -0
- package/src/components/estimates/create/prepare-estimate-submission.ts +39 -0
- package/src/components/estimates/create/use-estimate-customer-form.ts +5 -0
- package/src/components/estimates/estimates.hooks.ts +15 -0
- package/src/components/estimates/index.ts +6 -0
- package/src/components/estimates/list/index.ts +3 -0
- package/src/components/estimates/list/list-row-actions.tsx +103 -0
- package/src/components/estimates/list/list-table.tsx +171 -0
- package/src/components/estimates/list/locales/de.ts +26 -0
- package/src/components/estimates/list/locales/sl.ts +26 -0
- package/src/components/estimates/list/use-estimate-download.ts +63 -0
- package/src/components/export/document-export-form.tsx +288 -0
- package/src/components/export/index.ts +2 -0
- package/src/components/form/form-input.tsx +89 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/invoices/create/create-invoice-form.tsx +852 -0
- package/src/components/invoices/create/eslog-validation.test.ts +242 -0
- package/src/components/invoices/create/eslog-validation.ts +208 -0
- package/src/components/invoices/create/locales/de.ts +118 -0
- package/src/components/invoices/create/locales/sl.ts +114 -0
- package/src/components/invoices/create/prepare-invoice-submission.test.ts +777 -0
- package/src/components/invoices/create/prepare-invoice-submission.ts +79 -0
- package/src/components/invoices/create/use-invoice-customer-form.ts +5 -0
- package/src/components/invoices/index.ts +9 -0
- package/src/components/invoices/invoices-furs.hooks.ts +28 -0
- package/src/components/invoices/invoices.hooks.ts +110 -0
- package/src/components/invoices/list/index.ts +3 -0
- package/src/components/invoices/list/list-row-actions.tsx +132 -0
- package/src/components/invoices/list/list-table.tsx +165 -0
- package/src/components/invoices/list/locales/de.ts +33 -0
- package/src/components/invoices/list/locales/sl.ts +33 -0
- package/src/components/invoices/list/use-invoice-download.ts +62 -0
- package/src/components/invoices/send-email-dialog/index.ts +1 -0
- package/src/components/invoices/send-email-dialog/locales/de.ts +18 -0
- package/src/components/invoices/send-email-dialog/locales/sl.ts +17 -0
- package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +289 -0
- package/src/components/invoices/send-email-dialog.tsx +2 -0
- package/src/components/invoices/shared/index.ts +2 -0
- package/src/components/invoices/shared/scaled-document-preview.tsx +32 -0
- package/src/components/invoices/shared/use-a4-scaling.tsx +39 -0
- package/src/components/invoices/view/eslog-info-display.tsx +160 -0
- package/src/components/invoices/view/furs-info-display.tsx +213 -0
- package/src/components/items/create-item-form/create-item-form.tsx +155 -0
- package/src/components/items/create-item-form/locales/de.ts +14 -0
- package/src/components/items/create-item-form/locales/en.ts +9 -0
- package/src/components/items/create-item-form/locales/sl.ts +14 -0
- package/src/components/items/item-combobox.tsx +147 -0
- package/src/components/items/item-list-table/item-list-header.tsx +33 -0
- package/src/components/items/item-list-table/item-list-row-actions.tsx +48 -0
- package/src/components/items/item-list-table/item-list-row.tsx +32 -0
- package/src/components/items/item-list-table/item-list-table.tsx +76 -0
- package/src/components/items/item-list-table/locales/de.ts +10 -0
- package/src/components/items/item-list-table/locales/en.ts +10 -0
- package/src/components/items/item-list-table/locales/sl.ts +10 -0
- package/src/components/items/items.hooks.ts +63 -0
- package/src/components/loading-spinner.tsx +24 -0
- package/src/components/payments/create-payment-form/create-payment-form.tsx +222 -0
- package/src/components/payments/create-payment-form/locales/de.ts +20 -0
- package/src/components/payments/create-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/edit-payment-form/edit-payment-form.tsx +230 -0
- package/src/components/payments/edit-payment-form/index.ts +1 -0
- package/src/components/payments/edit-payment-form/locales/de.ts +20 -0
- package/src/components/payments/edit-payment-form/locales/sl.ts +20 -0
- package/src/components/payments/index.ts +4 -0
- package/src/components/payments/list/index.ts +2 -0
- package/src/components/payments/list/list-row-actions.tsx +98 -0
- package/src/components/payments/list/list-table.tsx +186 -0
- package/src/components/payments/list/locales/de.ts +19 -0
- package/src/components/payments/list/locales/sl.ts +19 -0
- package/src/components/payments/payments.hooks.ts +15 -0
- package/src/components/request-logs/index.ts +3 -0
- package/src/components/request-logs/request-log-detail.tsx +242 -0
- package/src/components/request-logs/request-log-list-table.tsx +266 -0
- package/src/components/request-logs/request-logs-page.tsx +10 -0
- package/src/components/table/README.md +410 -0
- package/src/components/table/data-table.tsx +251 -0
- package/src/components/table/date-cell.tsx +35 -0
- package/src/components/table/filter-bar.tsx +114 -0
- package/src/components/table/filter-panel.tsx +407 -0
- package/src/components/table/hooks/use-table-fetch.ts +17 -0
- package/src/components/table/hooks/use-table-query.ts +36 -0
- package/src/components/table/hooks/use-table-state.ts +293 -0
- package/src/components/table/index.ts +35 -0
- package/src/components/table/search-input.tsx +85 -0
- package/src/components/table/sortable-header.tsx +56 -0
- package/src/components/table/table-empty-state.tsx +40 -0
- package/src/components/table/table-no-results.tsx +41 -0
- package/src/components/table/table-pagination.tsx +42 -0
- package/src/components/table/table-skeleton.tsx +54 -0
- package/src/components/table/types.ts +136 -0
- package/src/components/tax-reports/index.ts +1 -0
- package/src/components/tax-reports/kir-export-form.tsx +172 -0
- package/src/components/taxes/create-tax-form/create-tax-form.tsx +112 -0
- package/src/components/taxes/create-tax-form/locales/de.ts +8 -0
- package/src/components/taxes/create-tax-form/locales/en.ts +7 -0
- package/src/components/taxes/create-tax-form/locales/sl.ts +8 -0
- package/src/components/taxes/tax-list-table/locales/de.ts +11 -0
- package/src/components/taxes/tax-list-table/locales/en.ts +10 -0
- package/src/components/taxes/tax-list-table/locales/sl.ts +11 -0
- package/src/components/taxes/tax-list-table/tax-list-header.tsx +29 -0
- package/src/components/taxes/tax-list-table/tax-list-row-actions.tsx +43 -0
- package/src/components/taxes/tax-list-table/tax-list-row.tsx +46 -0
- package/src/components/taxes/tax-list-table/tax-list-table.tsx +59 -0
- package/src/components/taxes/taxes.hooks.ts +35 -0
- package/src/components/ui/alert-dialog.tsx +61 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +132 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +213 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/chart.tsx +380 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +56 -0
- package/src/components/ui/command.tsx +187 -0
- package/src/components/ui/dialog.tsx +187 -0
- package/src/components/ui/drawer.tsx +123 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/form.tsx +166 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/loading-spinner.tsx +16 -0
- package/src/components/ui/popover.tsx +108 -0
- package/src/components/ui/radio-group.tsx +37 -0
- package/src/components/ui/select.tsx +200 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +145 -0
- package/src/components/ui/sidebar.tsx +771 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +60 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/sticky-form-footer.tsx +55 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +101 -0
- package/src/components/ui/tabs.tsx +80 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +89 -0
- package/src/components/wl-subscription/index.ts +2 -0
- package/src/components/wl-subscription/locked-feature.tsx +173 -0
- package/src/components/wl-subscription/upgrade-modal.tsx +209 -0
- package/src/frontend.tsx +28 -0
- package/src/generate-schemas.ts +265 -0
- package/src/generated/schemas/advanceinvoice.ts +177 -0
- package/src/generated/schemas/creditnote.ts +187 -0
- package/src/generated/schemas/customer.ts +29 -0
- package/src/generated/schemas/entity.ts +252 -0
- package/src/generated/schemas/estimate.ts +159 -0
- package/src/generated/schemas/furssettings.ts +25 -0
- package/src/generated/schemas/index.ts +24 -0
- package/src/generated/schemas/invoice.ts +167 -0
- package/src/generated/schemas/item.ts +38 -0
- package/src/generated/schemas/payment.ts +44 -0
- package/src/generated/schemas/previewadvanceinvoice_body.ts +354 -0
- package/src/generated/schemas/previewestimate_body.ts +309 -0
- package/src/generated/schemas/registerfursmovablepremise_body.ts +22 -0
- package/src/generated/schemas/registerfursrealestatepremise_body.ts +32 -0
- package/src/generated/schemas/renderdocument_body.ts +594 -0
- package/src/generated/schemas/sendemail_body.ts +26 -0
- package/src/generated/schemas/startpdfexport_body.ts +20 -0
- package/src/generated/schemas/tax.ts +48 -0
- package/src/generated/schemas/uploadfile_body.ts +23 -0
- package/src/generated/schemas/uploadfurscertificate_body.ts +20 -0
- package/src/generated/schemas/userfurssettings.ts +19 -0
- package/src/hooks/create-resource-hooks.test.ts +483 -0
- package/src/hooks/create-resource-hooks.ts +300 -0
- package/src/hooks/use-debounce.ts +12 -0
- package/src/hooks/use-duplicate-document.ts +185 -0
- package/src/hooks/use-media-query.tsx +19 -0
- package/src/hooks/use-mobile.ts +39 -0
- package/src/hooks/use-next-document-number.ts +57 -0
- package/src/hooks/use-resource-mutation.ts +118 -0
- package/src/hooks/use-vies-check.ts +130 -0
- package/src/index.css +11 -0
- package/src/index.html +13 -0
- package/src/index.tsx +12 -0
- package/src/lib/auth.ts +4 -0
- package/src/lib/browser-cookies.ts +70 -0
- package/src/lib/constants.ts +287 -0
- package/src/lib/cookies.ts +36 -0
- package/src/lib/schemas/advance-invoice.ts +43 -0
- package/src/lib/schemas/credit-note.ts +32 -0
- package/src/lib/schemas/estimate.ts +31 -0
- package/src/lib/schemas/index.ts +18 -0
- package/src/lib/schemas/invoice.ts +43 -0
- package/src/lib/schemas/shared.ts +79 -0
- package/src/lib/translation.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/entities-context.tsx +41 -0
- package/src/providers/entities-provider.tsx +201 -0
- package/src/providers/form-footer-context.tsx +72 -0
- package/src/providers/sdk-provider.tsx +164 -0
- package/src/providers/white-label-provider.tsx +91 -0
- package/src/providers/wl-subscription-provider.tsx +277 -0
- package/src/utils/string-helpers.ts +111 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { FilterState, HttpMethodFilter, HttpStatusCodeFilter, StatusFilter, TableQueryParams } from "../types";
|
|
3
|
+
|
|
4
|
+
type UseTableStateProps = {
|
|
5
|
+
initialParams?: TableQueryParams;
|
|
6
|
+
defaultOrderBy?: string;
|
|
7
|
+
onChangeParams?: (params: TableQueryParams) => void;
|
|
8
|
+
/** When true, disables URL sync entirely (for embedded tables like dashboard) */
|
|
9
|
+
disableUrlSync?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format date to YYYY-MM-DD for API (using local timezone)
|
|
14
|
+
*/
|
|
15
|
+
function formatDateForAPI(date: Date): string {
|
|
16
|
+
const year = date.getFullYear();
|
|
17
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
18
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
19
|
+
return `${year}-${month}-${day}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse YYYY-MM-DD string to local Date (avoids UTC parsing issues)
|
|
24
|
+
*/
|
|
25
|
+
function parseDateString(dateStr: string): Date {
|
|
26
|
+
const [year, month, day] = dateStr.split("-").map(Number);
|
|
27
|
+
return new Date(year, month - 1, day);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse filter state from URL-friendly params
|
|
32
|
+
*/
|
|
33
|
+
function parseFilterStateFromParams(params: TableQueryParams): FilterState | null {
|
|
34
|
+
const state: FilterState = {};
|
|
35
|
+
|
|
36
|
+
// Parse date filter
|
|
37
|
+
if (params.filter_date_field && (params.filter_date_from || params.filter_date_to)) {
|
|
38
|
+
state.dateFilter = {
|
|
39
|
+
field: params.filter_date_field,
|
|
40
|
+
range: {
|
|
41
|
+
from: params.filter_date_from ? parseDateString(params.filter_date_from) : undefined,
|
|
42
|
+
to: params.filter_date_to ? parseDateString(params.filter_date_to) : undefined,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse status filter
|
|
48
|
+
if (params.filter_status) {
|
|
49
|
+
state.statusFilters = params.filter_status.split(",") as StatusFilter[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Parse HTTP method filter
|
|
53
|
+
if (params.filter_method) {
|
|
54
|
+
state.httpMethod = params.filter_method as HttpMethodFilter;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse HTTP status code filter
|
|
58
|
+
if (params.filter_http_status) {
|
|
59
|
+
state.httpStatusCode = params.filter_http_status as HttpStatusCodeFilter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Object.keys(state).length > 0 ? state : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build API query JSON from filter state
|
|
67
|
+
* Note: API only supports flat field-level queries, not AND/OR operators
|
|
68
|
+
* For multiple statuses, only the first is used (API limitation)
|
|
69
|
+
*/
|
|
70
|
+
export function buildQueryFromFilterState(state: FilterState | null): string | undefined {
|
|
71
|
+
if (!state) return undefined;
|
|
72
|
+
|
|
73
|
+
const query: Record<string, unknown> = {};
|
|
74
|
+
|
|
75
|
+
// Date filter
|
|
76
|
+
if (state.dateFilter?.range.from || state.dateFilter?.range.to) {
|
|
77
|
+
const field = state.dateFilter.field;
|
|
78
|
+
const range = state.dateFilter.range;
|
|
79
|
+
|
|
80
|
+
if (range.from && range.to) {
|
|
81
|
+
query[field] = { between: [formatDateForAPI(range.from), formatDateForAPI(range.to)] };
|
|
82
|
+
} else if (range.from) {
|
|
83
|
+
query[field] = { gte: formatDateForAPI(range.from) };
|
|
84
|
+
} else if (range.to) {
|
|
85
|
+
query[field] = { lte: formatDateForAPI(range.to) };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Status filter - apply first selected status only (API doesn't support OR)
|
|
90
|
+
if (state.statusFilters?.length) {
|
|
91
|
+
const status = state.statusFilters[0]; // Use first status
|
|
92
|
+
const today = formatDateForAPI(new Date());
|
|
93
|
+
|
|
94
|
+
switch (status) {
|
|
95
|
+
case "paid":
|
|
96
|
+
query.paid_in_full = { equals: true };
|
|
97
|
+
break;
|
|
98
|
+
case "unpaid":
|
|
99
|
+
query.paid_in_full = { equals: false };
|
|
100
|
+
query.voided_at = { equals: null };
|
|
101
|
+
break;
|
|
102
|
+
case "overdue":
|
|
103
|
+
query.paid_in_full = { equals: false };
|
|
104
|
+
query.date_due = { lt: today };
|
|
105
|
+
query.voided_at = { equals: null };
|
|
106
|
+
break;
|
|
107
|
+
case "voided":
|
|
108
|
+
query.voided_at = { not: null };
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (Object.keys(query).length === 0) return undefined;
|
|
114
|
+
return JSON.stringify(query);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Manages table state (sorting, search, pagination) with optional URL sync
|
|
119
|
+
*/
|
|
120
|
+
export function useTableState({
|
|
121
|
+
initialParams = {},
|
|
122
|
+
defaultOrderBy = "-id",
|
|
123
|
+
onChangeParams,
|
|
124
|
+
disableUrlSync = false,
|
|
125
|
+
}: UseTableStateProps) {
|
|
126
|
+
const [params, setParams] = useState<TableQueryParams>({
|
|
127
|
+
...initialParams,
|
|
128
|
+
order_by: initialParams.order_by ?? defaultOrderBy,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Use ref for onChangeParams to keep it stable
|
|
132
|
+
const onChangeParamsRef = useRef(onChangeParams);
|
|
133
|
+
onChangeParamsRef.current = onChangeParams;
|
|
134
|
+
|
|
135
|
+
// Keep track of previous initialParams to detect changes
|
|
136
|
+
const prevInitialParamsRef = useRef<string>(JSON.stringify(initialParams));
|
|
137
|
+
// Flag to track if we're updating from initialParams (to avoid calling onChangeParams)
|
|
138
|
+
const isUpdatingFromInitialRef = useRef(false);
|
|
139
|
+
|
|
140
|
+
// Sync internal state when initialParams changes (e.g., when navigating to same page resets URL)
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const currentParamsStr = JSON.stringify(initialParams);
|
|
143
|
+
|
|
144
|
+
// Only update if initialParams actually changed
|
|
145
|
+
if (currentParamsStr !== prevInitialParamsRef.current) {
|
|
146
|
+
prevInitialParamsRef.current = currentParamsStr;
|
|
147
|
+
isUpdatingFromInitialRef.current = true;
|
|
148
|
+
setParams({
|
|
149
|
+
...initialParams,
|
|
150
|
+
order_by: initialParams.order_by ?? defaultOrderBy,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}, [initialParams, defaultOrderBy]);
|
|
154
|
+
|
|
155
|
+
// Sync params to parent or URL when they change
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
// Skip if we're updating from initialParams to avoid infinite loop
|
|
158
|
+
if (isUpdatingFromInitialRef.current) {
|
|
159
|
+
isUpdatingFromInitialRef.current = false;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Skip URL sync entirely when disabled (e.g., dashboard embedded tables)
|
|
164
|
+
if (disableUrlSync) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const changeHandler = onChangeParamsRef.current;
|
|
169
|
+
|
|
170
|
+
if (changeHandler) {
|
|
171
|
+
// Notify parent of param changes (e.g., for router navigation)
|
|
172
|
+
changeHandler(params);
|
|
173
|
+
} else {
|
|
174
|
+
// Update URL directly
|
|
175
|
+
const searchParams = new URLSearchParams();
|
|
176
|
+
|
|
177
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
178
|
+
if (value !== undefined && value !== null) {
|
|
179
|
+
searchParams.set(key, String(value));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
|
|
184
|
+
window.history.pushState({}, "", newUrl);
|
|
185
|
+
}
|
|
186
|
+
}, [params, disableUrlSync]);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Handle sort change
|
|
190
|
+
*/
|
|
191
|
+
const handleSort = useCallback(
|
|
192
|
+
(order: string | null) => {
|
|
193
|
+
setParams((prevParams) => ({
|
|
194
|
+
...prevParams,
|
|
195
|
+
order_by: order ?? defaultOrderBy,
|
|
196
|
+
prev_cursor: undefined,
|
|
197
|
+
next_cursor: undefined,
|
|
198
|
+
}));
|
|
199
|
+
},
|
|
200
|
+
[defaultOrderBy],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Handle search change
|
|
205
|
+
*/
|
|
206
|
+
const handleSearch = useCallback((value: string | null) => {
|
|
207
|
+
setParams((prevParams) => ({
|
|
208
|
+
...prevParams,
|
|
209
|
+
search: value?.trim() ?? undefined,
|
|
210
|
+
prev_cursor: undefined,
|
|
211
|
+
next_cursor: undefined,
|
|
212
|
+
}));
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle pagination change
|
|
217
|
+
*/
|
|
218
|
+
const handlePageChange = useCallback((cursor: { prev?: string; next?: string }) => {
|
|
219
|
+
setParams((prevParams) => ({
|
|
220
|
+
...prevParams,
|
|
221
|
+
next_cursor: cursor.next,
|
|
222
|
+
prev_cursor: cursor.next ? undefined : cursor.prev,
|
|
223
|
+
}));
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle filter change - stores URL-friendly params
|
|
228
|
+
*/
|
|
229
|
+
const handleFilterChange = useCallback((state: FilterState | null) => {
|
|
230
|
+
setParams((prevParams) => ({
|
|
231
|
+
...prevParams,
|
|
232
|
+
// Clear old filter params
|
|
233
|
+
filter_date_field: undefined,
|
|
234
|
+
filter_date_from: undefined,
|
|
235
|
+
filter_date_to: undefined,
|
|
236
|
+
filter_status: undefined,
|
|
237
|
+
filter_method: undefined,
|
|
238
|
+
filter_http_status: undefined,
|
|
239
|
+
// Set new filter params
|
|
240
|
+
...(state?.dateFilter && {
|
|
241
|
+
filter_date_field: state.dateFilter.field,
|
|
242
|
+
filter_date_from: state.dateFilter.range.from ? formatDateForAPI(state.dateFilter.range.from) : undefined,
|
|
243
|
+
filter_date_to: state.dateFilter.range.to ? formatDateForAPI(state.dateFilter.range.to) : undefined,
|
|
244
|
+
}),
|
|
245
|
+
...(state?.statusFilters?.length && {
|
|
246
|
+
filter_status: state.statusFilters.join(","),
|
|
247
|
+
}),
|
|
248
|
+
...(state?.httpMethod && {
|
|
249
|
+
filter_method: state.httpMethod,
|
|
250
|
+
}),
|
|
251
|
+
...(state?.httpStatusCode && {
|
|
252
|
+
filter_http_status: state.httpStatusCode,
|
|
253
|
+
}),
|
|
254
|
+
prev_cursor: undefined,
|
|
255
|
+
next_cursor: undefined,
|
|
256
|
+
}));
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Parse current filter state from URL params
|
|
261
|
+
*/
|
|
262
|
+
const filterState = useMemo(() => {
|
|
263
|
+
return parseFilterStateFromParams(params);
|
|
264
|
+
}, [
|
|
265
|
+
params.filter_date_field,
|
|
266
|
+
params.filter_date_from,
|
|
267
|
+
params.filter_date_to,
|
|
268
|
+
params.filter_status,
|
|
269
|
+
params.filter_method,
|
|
270
|
+
params.filter_http_status,
|
|
271
|
+
params,
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Build params for API call (includes query JSON built from filter state)
|
|
276
|
+
* Note: filter_* params are kept for tables that need them (like request logs)
|
|
277
|
+
* while query JSON is added for tables that use it (like invoices)
|
|
278
|
+
*/
|
|
279
|
+
const apiParams = useMemo(() => {
|
|
280
|
+
const query = buildQueryFromFilterState(filterState);
|
|
281
|
+
return { ...params, query };
|
|
282
|
+
}, [params, filterState]);
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
params,
|
|
286
|
+
apiParams,
|
|
287
|
+
filterState,
|
|
288
|
+
handleSort,
|
|
289
|
+
handleSearch,
|
|
290
|
+
handlePageChange,
|
|
291
|
+
handleFilterChange,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table Component Library
|
|
3
|
+
*
|
|
4
|
+
* A comprehensive, type-safe table system with built-in:
|
|
5
|
+
* - Sorting (client and server-side)
|
|
6
|
+
* - Search/filtering
|
|
7
|
+
* - Cursor-based pagination
|
|
8
|
+
* - Loading states
|
|
9
|
+
* - Empty states
|
|
10
|
+
* - TanStack Query integration
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type { DataTableProps } from "./data-table";
|
|
14
|
+
// Main component
|
|
15
|
+
export { DataTable } from "./data-table";
|
|
16
|
+
export { FormattedDate } from "./date-cell";
|
|
17
|
+
// Hooks
|
|
18
|
+
export { useTableFetch } from "./hooks/use-table-fetch";
|
|
19
|
+
export { useTableQuery } from "./hooks/use-table-query";
|
|
20
|
+
export { useTableState } from "./hooks/use-table-state";
|
|
21
|
+
// Supporting components
|
|
22
|
+
export { SearchInput } from "./search-input";
|
|
23
|
+
export { SortableHeader } from "./sortable-header";
|
|
24
|
+
export { TableEmptyState } from "./table-empty-state";
|
|
25
|
+
export { TableNoResults } from "./table-no-results";
|
|
26
|
+
export { Pagination } from "./table-pagination";
|
|
27
|
+
export { TableSkeleton } from "./table-skeleton";
|
|
28
|
+
|
|
29
|
+
// Types
|
|
30
|
+
export type {
|
|
31
|
+
Column,
|
|
32
|
+
ListTableProps,
|
|
33
|
+
TableQueryParams,
|
|
34
|
+
TableQueryResponse,
|
|
35
|
+
} from "./types";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Search, X } from "lucide-react";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "@/ui/components/ui/button";
|
|
5
|
+
import { Input } from "@/ui/components/ui/input";
|
|
6
|
+
|
|
7
|
+
type SearchInputProps = {
|
|
8
|
+
initialValue?: string;
|
|
9
|
+
onSearch: (value: string | null) => void;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
debounceMs?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Search input with optional clear button and form submission
|
|
16
|
+
*/
|
|
17
|
+
export function SearchInput({ initialValue = "", onSearch, placeholder = "Search...", debounceMs }: SearchInputProps) {
|
|
18
|
+
const [value, setValue] = useState(initialValue);
|
|
19
|
+
|
|
20
|
+
// Use ref to keep onSearch stable in useEffect
|
|
21
|
+
const onSearchRef = useRef(onSearch);
|
|
22
|
+
onSearchRef.current = onSearch;
|
|
23
|
+
|
|
24
|
+
// Sync with external value changes
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setValue(initialValue);
|
|
27
|
+
}, [initialValue]);
|
|
28
|
+
|
|
29
|
+
// Optional debounced search
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (debounceMs === undefined) return;
|
|
32
|
+
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
onSearchRef.current(value || null);
|
|
35
|
+
}, debounceMs);
|
|
36
|
+
|
|
37
|
+
return () => clearTimeout(timer);
|
|
38
|
+
}, [value, debounceMs]);
|
|
39
|
+
|
|
40
|
+
const handleSubmit = useCallback(
|
|
41
|
+
(e: React.FormEvent) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
if (debounceMs === undefined) {
|
|
44
|
+
onSearchRef.current(value || null);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
[debounceMs, value],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
51
|
+
setValue(e.target.value);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const handleClear = useCallback(() => {
|
|
55
|
+
setValue("");
|
|
56
|
+
onSearchRef.current(null);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<form onSubmit={handleSubmit} className="relative inline-block" data-testid="search-form">
|
|
61
|
+
<Search className="pointer-events-none absolute top-1/2 left-2.5 z-10 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
|
|
62
|
+
<Input
|
|
63
|
+
type="search"
|
|
64
|
+
role="searchbox"
|
|
65
|
+
aria-label="Search"
|
|
66
|
+
placeholder={placeholder}
|
|
67
|
+
value={value}
|
|
68
|
+
onChange={handleChange}
|
|
69
|
+
className="h-8 w-[150px] pr-8 pl-8 lg:w-[250px] [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden"
|
|
70
|
+
/>
|
|
71
|
+
{value && (
|
|
72
|
+
<Button
|
|
73
|
+
type="button"
|
|
74
|
+
variant="ghost"
|
|
75
|
+
size="sm"
|
|
76
|
+
onClick={handleClear}
|
|
77
|
+
className="absolute top-1/2 right-1 z-10 h-6 w-6 -translate-y-1/2 p-0 hover:bg-transparent"
|
|
78
|
+
aria-label="Clear search"
|
|
79
|
+
>
|
|
80
|
+
<X className="h-3 w-3 text-muted-foreground" />
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</form>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/ui/components/ui/button";
|
|
4
|
+
import { cn } from "@/ui/lib/utils";
|
|
5
|
+
|
|
6
|
+
type SortableHeaderProps = {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
field: string;
|
|
9
|
+
currentOrder?: string;
|
|
10
|
+
align?: "left" | "center" | "right";
|
|
11
|
+
onSort?: (order: string | null) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sortable column header with visual indicators
|
|
16
|
+
* Cycles through: none -> asc -> desc -> none
|
|
17
|
+
*/
|
|
18
|
+
export function SortableHeader({ children, field, currentOrder, align = "left", onSort }: SortableHeaderProps) {
|
|
19
|
+
const isActive = currentOrder?.replace(/^-/, "") === field;
|
|
20
|
+
const isDesc = currentOrder?.startsWith("-");
|
|
21
|
+
|
|
22
|
+
// Determine next sort state
|
|
23
|
+
const getNextOrder = () => {
|
|
24
|
+
if (!isActive) return field; // Not active -> ascending
|
|
25
|
+
if (!isDesc) return `-${field}`; // Ascending -> descending
|
|
26
|
+
return null; // Descending -> none
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const nextOrder = getNextOrder();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Button
|
|
33
|
+
variant="ghost"
|
|
34
|
+
size="sm"
|
|
35
|
+
onClick={() => onSort?.(nextOrder)}
|
|
36
|
+
className={cn(
|
|
37
|
+
"h-8 gap-1",
|
|
38
|
+
isActive && "font-semibold",
|
|
39
|
+
align === "left" && "-ml-3",
|
|
40
|
+
align === "right" && "-mr-3",
|
|
41
|
+
)}
|
|
42
|
+
aria-label={`Sort by ${field}${isActive ? (isDesc ? " descending" : " ascending") : ""}`}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
{isActive ? (
|
|
46
|
+
isDesc ? (
|
|
47
|
+
<ArrowDown className="h-4 w-4" aria-hidden="true" />
|
|
48
|
+
) : (
|
|
49
|
+
<ArrowUp className="h-4 w-4" aria-hidden="true" />
|
|
50
|
+
)
|
|
51
|
+
) : (
|
|
52
|
+
<ArrowUpDown className="h-4 w-4 opacity-50" aria-hidden="true" />
|
|
53
|
+
)}
|
|
54
|
+
</Button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Sprout } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/ui/components/ui/button";
|
|
4
|
+
|
|
5
|
+
type TableEmptyStateProps = {
|
|
6
|
+
resource: string;
|
|
7
|
+
createNewLink?: string;
|
|
8
|
+
createNewTrigger?: React.ReactNode;
|
|
9
|
+
/** Number of rows to calculate height (default: 10) */
|
|
10
|
+
rows?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Approximate row height in pixels (including padding/border)
|
|
14
|
+
const ROW_HEIGHT = 53;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Empty state shown when table has no data
|
|
18
|
+
*/
|
|
19
|
+
export function TableEmptyState({ resource, createNewLink, createNewTrigger, rows = 10 }: TableEmptyStateProps) {
|
|
20
|
+
// Calculate height based on row count (min 200px)
|
|
21
|
+
const height = Math.max(rows * ROW_HEIGHT, 200);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex flex-col items-center justify-center gap-3" style={{ height }}>
|
|
25
|
+
<Sprout size={70} strokeWidth={0.35} className="text-muted-foreground" />
|
|
26
|
+
<div className="space-y-1 text-center">
|
|
27
|
+
<p className="font-light text-lg text-muted-foreground">Your {resource} list is empty</p>
|
|
28
|
+
{(createNewLink || createNewTrigger) && (
|
|
29
|
+
<p className="text-muted-foreground text-sm">Get started by creating your first {resource}</p>
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
{createNewLink && (
|
|
33
|
+
<Button variant="default" size="sm" asChild>
|
|
34
|
+
<a href={createNewLink}>Create {resource}</a>
|
|
35
|
+
</Button>
|
|
36
|
+
)}
|
|
37
|
+
{createNewTrigger && !createNewLink && <div>{createNewTrigger}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { FileX } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/ui/components/ui/button";
|
|
4
|
+
import { TableCell, TableRow } from "@/ui/components/ui/table";
|
|
5
|
+
|
|
6
|
+
type TableNoResultsProps = {
|
|
7
|
+
resource: string;
|
|
8
|
+
search?: (value: null) => void;
|
|
9
|
+
/** Number of rows to calculate height (default: 10) */
|
|
10
|
+
rows?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Approximate row height in pixels (including padding/border)
|
|
14
|
+
const ROW_HEIGHT = 53;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* No results message shown when search returns empty
|
|
18
|
+
*/
|
|
19
|
+
export function TableNoResults({ resource, search, rows = 10 }: TableNoResultsProps) {
|
|
20
|
+
// Calculate height based on row count (min 150px)
|
|
21
|
+
const height = Math.max(rows * ROW_HEIGHT, 150);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<TableRow className="hover:bg-transparent">
|
|
25
|
+
<TableCell colSpan={100} className="text-center align-middle" style={{ height }}>
|
|
26
|
+
<div className="flex flex-col items-center gap-3">
|
|
27
|
+
<FileX size={32} strokeWidth={1.5} className="text-muted-foreground" />
|
|
28
|
+
<div className="space-y-1">
|
|
29
|
+
<p className="font-medium text-muted-foreground">No {resource} found</p>
|
|
30
|
+
{search && <p className="text-muted-foreground text-sm">Try adjusting your search criteria</p>}
|
|
31
|
+
</div>
|
|
32
|
+
{search && (
|
|
33
|
+
<Button variant="link" size="sm" onClick={() => search(null)} className="underline">
|
|
34
|
+
Clear search
|
|
35
|
+
</Button>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
</TableCell>
|
|
39
|
+
</TableRow>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/ui/components/ui/button";
|
|
4
|
+
|
|
5
|
+
type PaginationProps = {
|
|
6
|
+
prevCursor?: string | null;
|
|
7
|
+
nextCursor?: string | null;
|
|
8
|
+
onPageChange: (cursor: { prev?: string; next?: string }) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cursor-based pagination controls
|
|
13
|
+
*/
|
|
14
|
+
export function Pagination({ prevCursor, nextCursor, onPageChange }: PaginationProps) {
|
|
15
|
+
const hasPrevious = Boolean(prevCursor);
|
|
16
|
+
const hasNext = Boolean(nextCursor);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center justify-end space-x-2">
|
|
20
|
+
<Button
|
|
21
|
+
variant="outline"
|
|
22
|
+
size="sm"
|
|
23
|
+
className="h-8 w-8 cursor-pointer p-0"
|
|
24
|
+
onClick={() => onPageChange({ prev: prevCursor ?? undefined })}
|
|
25
|
+
disabled={!hasPrevious}
|
|
26
|
+
aria-label="Previous page"
|
|
27
|
+
>
|
|
28
|
+
<ChevronLeft className="h-4 w-4" />
|
|
29
|
+
</Button>
|
|
30
|
+
<Button
|
|
31
|
+
variant="outline"
|
|
32
|
+
size="sm"
|
|
33
|
+
className="h-8 w-8 cursor-pointer p-0"
|
|
34
|
+
onClick={() => onPageChange({ next: nextCursor ?? undefined })}
|
|
35
|
+
disabled={!hasNext}
|
|
36
|
+
aria-label="Next page"
|
|
37
|
+
>
|
|
38
|
+
<ChevronRight className="h-4 w-4" />
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Skeleton } from "@/ui/components/ui/skeleton";
|
|
2
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/ui/components/ui/table";
|
|
3
|
+
|
|
4
|
+
type TableSkeletonProps = {
|
|
5
|
+
columns?: number;
|
|
6
|
+
rows?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Loading skeleton for table component
|
|
11
|
+
*/
|
|
12
|
+
export function TableSkeleton({ columns = 5, rows = 10 }: TableSkeletonProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-4">
|
|
15
|
+
{/* Search input skeleton */}
|
|
16
|
+
<Skeleton className="h-8 w-[250px]" />
|
|
17
|
+
|
|
18
|
+
{/* Table skeleton */}
|
|
19
|
+
<div className="rounded-lg border">
|
|
20
|
+
<Table>
|
|
21
|
+
<TableHeader>
|
|
22
|
+
<TableRow>
|
|
23
|
+
{Array.from({ length: columns }, (_, i) => (
|
|
24
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton
|
|
25
|
+
<TableHead key={`header-${i}`}>
|
|
26
|
+
<Skeleton data-testid="skeleton-cell" className="my-2 h-4 w-[100px]" />
|
|
27
|
+
</TableHead>
|
|
28
|
+
))}
|
|
29
|
+
</TableRow>
|
|
30
|
+
</TableHeader>
|
|
31
|
+
<TableBody>
|
|
32
|
+
{Array.from({ length: rows }, (_, rowIndex) => (
|
|
33
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton
|
|
34
|
+
<TableRow key={`row-${rowIndex}`}>
|
|
35
|
+
{Array.from({ length: columns }, (_, colIndex) => (
|
|
36
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton
|
|
37
|
+
<TableCell key={`cell-${rowIndex}-${colIndex}`}>
|
|
38
|
+
<Skeleton data-testid="skeleton-cell" className="my-2 h-4 w-[100px]" />
|
|
39
|
+
</TableCell>
|
|
40
|
+
))}
|
|
41
|
+
</TableRow>
|
|
42
|
+
))}
|
|
43
|
+
</TableBody>
|
|
44
|
+
</Table>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Pagination skeleton */}
|
|
48
|
+
<div className="flex justify-end gap-2">
|
|
49
|
+
<Skeleton className="h-8 w-8" />
|
|
50
|
+
<Skeleton className="h-8 w-8" />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|