@graph8/sdk 0.361.0 → 0.370.0
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/README.md +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -27,6 +27,7 @@ import { useObjectSchema } from "../hooks/use-object-schema";
|
|
|
27
27
|
import { OBJECT_ICONS, isObjectsApiMissing, isValidSlug, slugify } from "../lib/object-types";
|
|
28
28
|
import type { ObjectAttribute, ObjectDefinition } from "../lib/object-types";
|
|
29
29
|
import { AttributeFormDialog } from "./attribute-form-dialog";
|
|
30
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
30
31
|
|
|
31
32
|
export interface ObjectSchemaEditorProps {
|
|
32
33
|
/** Object to open first. */
|
|
@@ -93,6 +94,7 @@ function ObjectDetailsForm({ object, attributes, onSaved }: { object: ObjectDefi
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
export function ObjectSchemaEditor({ initialObjectSlug, onSelectObject }: ObjectSchemaEditorProps) {
|
|
97
|
+
const brand = useBrand();
|
|
96
98
|
const [selected, setSelected] = useState<string | undefined>(initialObjectSlug);
|
|
97
99
|
const [editing, setEditing] = useState<ObjectAttribute | "new" | null>(null);
|
|
98
100
|
const [pending, setPending] = useState<Pending | null>(null);
|
|
@@ -113,7 +115,7 @@ export function ObjectSchemaEditor({ initialObjectSlug, onSelectObject }: Object
|
|
|
113
115
|
|
|
114
116
|
if (objects.loading && !objects.data) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-48" /><Skeleton className="h-40 w-full" /></div>;
|
|
115
117
|
if (isObjectsApiMissing(objects.error)) {
|
|
116
|
-
return <EmptyState title="Custom objects are unavailable" description=
|
|
118
|
+
return <EmptyState title="Custom objects are unavailable" description={`The ${brand.linkLabel} API at this base URL does not serve custom objects. Check G8_API_URL and that it points at a current ${brand.linkLabel} backend, then reload.`} />;
|
|
117
119
|
}
|
|
118
120
|
if (objects.error) return <ApiError error={objects.error} title="Could not load objects" onRetry={() => void objects.refetch()} />;
|
|
119
121
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { useEffect, useState } from "react";
|
|
16
|
+
import type { ReactNode } from "react";
|
|
16
17
|
import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
17
18
|
import { ApiError } from "@/g8/components/api-error";
|
|
18
19
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -20,6 +21,8 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
20
21
|
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
21
22
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
23
|
import { usePaginated } from "@/g8/hooks/use-paginated";
|
|
24
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
25
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
23
26
|
import { useObjectSchema } from "../hooks/use-object-schema";
|
|
24
27
|
import { formatValue, isObjectsApiMissing, recordTitle } from "../lib/object-types";
|
|
25
28
|
import { ObjectNotFound } from "./object-not-found";
|
|
@@ -27,6 +30,8 @@ import type { ObjectRecord } from "../lib/object-types";
|
|
|
27
30
|
import { RecordFormDialog } from "./record-form-dialog";
|
|
28
31
|
|
|
29
32
|
export interface ObjectTableProps {
|
|
33
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
34
|
+
linkParam?: string;
|
|
30
35
|
objectSlug: string;
|
|
31
36
|
/** Rows per request, 1-200. Default 50. */
|
|
32
37
|
pageSize?: number;
|
|
@@ -36,7 +41,7 @@ export interface ObjectTableProps {
|
|
|
36
41
|
maxColumns?: number;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColumns = 6 }: ObjectTableProps) {
|
|
44
|
+
export function ObjectTable({ linkParam, objectSlug, pageSize = 50, onSelectRecord, maxColumns = 6 }: ObjectTableProps) {
|
|
40
45
|
const schema = useObjectSchema(objectSlug);
|
|
41
46
|
const slug = encodeURIComponent(objectSlug);
|
|
42
47
|
const records = usePaginated<ObjectRecord>(
|
|
@@ -68,6 +73,11 @@ export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColu
|
|
|
68
73
|
return <ApiError error={schema.error ?? records.error} title="Could not load records" onRetry={() => { void schema.refetch(); void records.reload(); }} />;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
const cellText = (c: (typeof columns)[number], i: number, record: ObjectRecord): ReactNode => {
|
|
77
|
+
const text = formatValue(c, record.values?.[c.slug]) || (i === 0 ? recordTitle(schema.definition, schema.attributes, record) : "");
|
|
78
|
+
return i === 0 && onSelectRecord && text ? <RecordLink param={linkParam} id={record.id} onOpen={() => onSelectRecord(record)}>{text}</RecordLink> : text;
|
|
79
|
+
};
|
|
80
|
+
|
|
71
81
|
return (
|
|
72
82
|
<div className="grid gap-3">
|
|
73
83
|
<div className="flex items-center justify-between gap-2">
|
|
@@ -92,9 +102,9 @@ export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColu
|
|
|
92
102
|
</TableHeader>
|
|
93
103
|
<TableBody>
|
|
94
104
|
{rows.map((record) => (
|
|
95
|
-
<TableRow key={record.id} className={onSelectRecord ? "cursor-pointer" : undefined}
|
|
105
|
+
<TableRow key={record.id} className={onSelectRecord ? "cursor-pointer" : undefined} {...(onSelectRecord ? rowActivationProps(() => onSelectRecord(record)) : {})}>
|
|
96
106
|
{columns.map((c, i) => (
|
|
97
|
-
<TableCell key={c.slug}>{
|
|
107
|
+
<TableCell key={c.slug}>{cellText(c, i, record)}</TableCell>
|
|
98
108
|
))}
|
|
99
109
|
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
|
100
110
|
<Button variant="ghost" size="sm" onClick={() => setForm({ record })}>Edit</Button>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ObjectsScreen: the finished custom-objects workspace (HubSpot custom objects / Salesforce objects). Pick an object, work its
|
|
5
|
+
* records, open one for its values, owners, relationships and history, and define new objects and fields. No props are required.
|
|
6
|
+
*
|
|
7
|
+
* Composes: ScreenLayout, StatStrip, SplitView, FormSheet (screen kit) with ObjectTable, ObjectRecordDetail and
|
|
8
|
+
* ObjectSchemaEditor (this block). Standard objects (contacts, companies, deals) appear next to your own.
|
|
9
|
+
*
|
|
10
|
+
* Flows: switch between objects (deep link ?object=) - search the loaded rows, load more - create, edit and archive a record
|
|
11
|
+
* - open a record (deep link ?id=, full page with &view=full) to see values, owners, relationships and changes, edit, archive or restore it - define a
|
|
12
|
+
* new object, add, edit and archive fields, rename an object ("Define objects", needs an Admin key with objects:manage).
|
|
13
|
+
*
|
|
14
|
+
* SDK operations: none of its own. listObjects (picker, stats) is `read`; everything else runs inside the parts it composes,
|
|
15
|
+
* and every archive (record, field, object) goes through ConfirmSpendDialog. A 404 on the list call means the backend does not
|
|
16
|
+
* serve custom objects; the screen says so instead of showing an empty picker.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
22
|
+
import { Box, Maximize2, Minimize2, Settings2 } from "lucide-react";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
26
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
27
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
28
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
29
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
30
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
31
|
+
import { useObjectSchema } from "../hooks/use-object-schema";
|
|
32
|
+
import { isObjectsApiMissing } from "../lib/object-types";
|
|
33
|
+
import type { ObjectDefinition } from "../lib/object-types";
|
|
34
|
+
import { ObjectNotFound } from "./object-not-found";
|
|
35
|
+
import { ObjectRecordDetail } from "./object-record-detail";
|
|
36
|
+
import { ObjectSchemaEditor } from "./object-schema-editor";
|
|
37
|
+
import { ObjectTable } from "./object-table";
|
|
38
|
+
|
|
39
|
+
export interface ObjectsScreenProps {
|
|
40
|
+
/** Object to open when the URL names none. Default: the first non-standard object, else the first one. */
|
|
41
|
+
initialObjectSlug?: string;
|
|
42
|
+
/** Show "Define objects" (the schema editor). Default true; it needs an Admin key behind the proxy. */
|
|
43
|
+
allowSchemaEditing?: boolean;
|
|
44
|
+
/** Extra buttons in the page header. */
|
|
45
|
+
toolbarActions?: ReactNode;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function ObjectsScreen({ initialObjectSlug, allowSchemaEditing = true, toolbarActions, className }: ObjectsScreenProps) {
|
|
50
|
+
const [objectParam, setObjectParam] = useUrlState("object");
|
|
51
|
+
const [recordId, setRecordIdRaw] = useUrlState("id");
|
|
52
|
+
const [view, setView] = useUrlState("view");
|
|
53
|
+
const full = view === "full" && Boolean(recordId);
|
|
54
|
+
const setRecordId = (v: string | null): void => { if (v == null) setView(null); setRecordIdRaw(v); };
|
|
55
|
+
const [schemaOpen, setSchemaOpen] = useState(false);
|
|
56
|
+
const [tableKey, setTableKey] = useState(0);
|
|
57
|
+
const objects = useG8Query((signal) => g8Get<ObjectDefinition[]>("/objects", {}, { signal }).then((r) => r.data ?? []), []);
|
|
58
|
+
const all = (objects.data ?? []).filter((o) => !o.is_archived);
|
|
59
|
+
const slug = objectParam || initialObjectSlug || (all.find((o) => !o.is_system) ?? all[0])?.slug || "";
|
|
60
|
+
const current = all.find((o) => o.slug === slug);
|
|
61
|
+
const schema = useObjectSchema(slug || undefined);
|
|
62
|
+
|
|
63
|
+
const closeSchema = (open: boolean): void => {
|
|
64
|
+
setSchemaOpen(open);
|
|
65
|
+
if (open) return;
|
|
66
|
+
void objects.refetch();
|
|
67
|
+
void schema.refetch();
|
|
68
|
+
setTableKey((k) => k + 1);
|
|
69
|
+
};
|
|
70
|
+
const pick = (next: string) => { setObjectParam(next); setRecordId(null); };
|
|
71
|
+
|
|
72
|
+
if (isObjectsApiMissing(objects.error)) return <ObjectNotFound what="object" error={objects.error} />;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<ScreenLayout
|
|
76
|
+
className={className}
|
|
77
|
+
title={current?.plural_noun ?? "Objects"}
|
|
78
|
+
description="Records of any type you define, next to your standard contacts, companies and deals."
|
|
79
|
+
icon={<Box className="size-5" aria-hidden />}
|
|
80
|
+
actions={
|
|
81
|
+
<>
|
|
82
|
+
{toolbarActions}
|
|
83
|
+
{allowSchemaEditing ? <Button variant="outline" size="sm" onClick={() => setSchemaOpen(true)}><Settings2 className="size-4" aria-hidden />Define objects</Button> : null}
|
|
84
|
+
</>
|
|
85
|
+
}
|
|
86
|
+
stats={[
|
|
87
|
+
{ label: "Objects", value: objects.loading && !objects.data ? "-" : all.length, hint: `${all.filter((o) => !o.is_system).length} custom` },
|
|
88
|
+
{ label: "Fields on this object", value: slug && !schema.loading ? schema.attributes.length : "-" },
|
|
89
|
+
]}
|
|
90
|
+
>
|
|
91
|
+
{objects.error ? <ApiError error={objects.error} title="Could not load objects" onRetry={() => void objects.refetch()} /> : null}
|
|
92
|
+
{objects.loading && !objects.data ? <Skeleton className="h-9 w-full" /> : null}
|
|
93
|
+
|
|
94
|
+
{all.length > 0 ? (
|
|
95
|
+
<div className="mb-3 flex flex-wrap gap-2" role="group" aria-label="Objects">
|
|
96
|
+
{all.map((o) => (
|
|
97
|
+
<Button key={o.slug} variant={o.slug === slug ? "default" : "outline"} size="sm" aria-pressed={o.slug === slug} onClick={() => pick(o.slug)}>
|
|
98
|
+
{o.plural_noun}{o.is_system ? <span className="ml-1 text-xs opacity-70">standard</span> : null}
|
|
99
|
+
</Button>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
) : null}
|
|
103
|
+
|
|
104
|
+
{!objects.loading && !objects.error && all.length === 0 ? (
|
|
105
|
+
<EmptyState
|
|
106
|
+
title="No objects yet"
|
|
107
|
+
description="Define an object such as an invoice or a shipment, give it fields, then add records."
|
|
108
|
+
action={allowSchemaEditing ? <Button onClick={() => setSchemaOpen(true)}>Define your first object</Button> : undefined}
|
|
109
|
+
/>
|
|
110
|
+
) : null}
|
|
111
|
+
|
|
112
|
+
{slug ? (
|
|
113
|
+
<SplitView
|
|
114
|
+
detailOpen={Boolean(recordId)}
|
|
115
|
+
expanded={full}
|
|
116
|
+
onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
117
|
+
onCloseDetail={() => setRecordId(null)}
|
|
118
|
+
detailWidth="lg"
|
|
119
|
+
list={<ObjectTable linkParam="id" key={`${slug}:${tableKey}`} objectSlug={slug} onSelectRecord={(r) => setRecordId(r.id)} />}
|
|
120
|
+
detail={recordId ? (
|
|
121
|
+
<div className="flex flex-col gap-2">
|
|
122
|
+
<div className="flex justify-end">
|
|
123
|
+
<Button variant="ghost" size="sm" onClick={() => setView(full ? null : "full")}>
|
|
124
|
+
{full ? <Minimize2 className="size-4" aria-hidden /> : <Maximize2 className="size-4" aria-hidden />}{full ? "Back to split view" : "Open full page"}
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
<ObjectRecordDetail
|
|
128
|
+
key={`${slug}:${recordId}`}
|
|
129
|
+
objectSlug={slug}
|
|
130
|
+
recordId={recordId}
|
|
131
|
+
onBack={() => setRecordId(null)}
|
|
132
|
+
onArchived={() => { setRecordId(null); setTableKey((k) => k + 1); }}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
) : null}
|
|
136
|
+
/>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
139
|
+
<FormSheet open={schemaOpen} onOpenChange={closeSchema} title="Define objects" description="Create objects, add fields, rename or archive them. Needs an Admin key.">
|
|
140
|
+
<ObjectSchemaEditor
|
|
141
|
+
initialObjectSlug={slug || undefined}
|
|
142
|
+
onSelectObject={pick}
|
|
143
|
+
/>
|
|
144
|
+
</FormSheet>
|
|
145
|
+
</ScreenLayout>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* an attribute definition into the right input, display text and API value.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { G8Error } from "@graph8/sdk";
|
|
11
|
+
import { G8Error } from "@graph8/sdk/errors";
|
|
12
12
|
|
|
13
13
|
export type AttributeType =
|
|
14
14
|
| "text" | "number" | "currency" | "date" | "timestamp" | "checkbox" | "select" | "status"
|
package/blocks/pages/README.md
CHANGED
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
Editable React screens for the marketing-site side of a product: landing pages (list, section editor, publish),
|
|
4
4
|
a content library, the brand kit and SEO audits. Install with `npx @graph8/sdk add pages`.
|
|
5
5
|
|
|
6
|
+
## Screens
|
|
7
|
+
|
|
8
|
+
`PagesScreen` (`components/pages-screen.tsx`) is the finished page: landing pages with the editor beside the list, plus content, brand kit and SEO tabs.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { PagesScreen } from "@/g8/pages/components/pages-screen";
|
|
12
|
+
export default function Page() { return <PagesScreen />; }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Flows: list and filter landing pages, create blank (free) or with AI (confirmed spend), open one (deep link `?id=`) to edit sections, preview, restore a version, publish or unpublish (confirmed) and delete (confirmed); manage the content library; re-scan the brand kit; run SEO audits. Needs the proxy prefixes listed under Prerequisites. Optional props: `defaultDomain`, `renderDetailActions`, `extraDetailTabs`, `toolbarActions`.
|
|
16
|
+
|
|
6
17
|
| Component | What it is |
|
|
7
18
|
|---|---|
|
|
19
|
+
| `PagesScreen` | The finished page: landing pages with the editor beside them, content, brand kit and SEO tabs. |
|
|
8
20
|
| `PageList` | Landing pages with a status filter; create blank or with AI; publish, unpublish, delete. |
|
|
9
21
|
| `PageEditor` | Lite editor: page details, one card per section (its text fields), preview, version restore, publish. Not a WYSIWYG canvas. |
|
|
10
22
|
| `ContentLibrary` | Content inventory (blogs, website pages): filters, add by URL, edit sheet, SEO enrichment, delete. |
|
package/blocks/pages/block.json
CHANGED
|
@@ -14,37 +14,49 @@
|
|
|
14
14
|
"foundation"
|
|
15
15
|
],
|
|
16
16
|
"operations": [
|
|
17
|
-
"
|
|
17
|
+
"create_content_page_content_pages_post",
|
|
18
18
|
"create_landing_page_landing_pages_post",
|
|
19
|
-
"publish_landing_page_landing_pages__landing_page_id__publish_post",
|
|
20
|
-
"unpublish_landing_page_landing_pages__landing_page_id__unpublish_post",
|
|
21
|
-
"delete_landing_page_landing_pages__landing_page_id__delete",
|
|
22
|
-
"get_landing_page_landing_pages__landing_page_id__get",
|
|
23
|
-
"update_landing_page_landing_pages__landing_page_id__patch",
|
|
24
|
-
"update_landing_page_section_landing_pages__landing_page_id__sections__section_idx__patch",
|
|
25
19
|
"create_landing_page_preview_link_landing_pages__landing_page_id__preview_get",
|
|
26
|
-
"list_landing_page_versions_landing_pages__landing_page_id__versions_get",
|
|
27
|
-
"restore_landing_page_version_landing_pages__landing_page_id__versions__version_num__restore_post",
|
|
28
|
-
"list_content_pages_content_pages_get",
|
|
29
|
-
"create_content_page_content_pages_post",
|
|
30
|
-
"get_content_page_content_pages__page_id__get",
|
|
31
|
-
"update_content_page_content_pages__page_id__patch",
|
|
32
|
-
"enrich_content_page_content_pages__page_id__enrich_seo_post",
|
|
33
20
|
"delete_content_page_content_pages__page_id__delete",
|
|
21
|
+
"delete_landing_page_landing_pages__landing_page_id__delete",
|
|
22
|
+
"enrich_content_page_content_pages__page_id__enrich_seo_post",
|
|
34
23
|
"get_brand_kit_overview_brand_kit_get",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
24
|
+
"get_content_page_content_pages__page_id__get",
|
|
25
|
+
"get_landing_page_landing_pages__landing_page_id__get",
|
|
37
26
|
"get_layout_selections_brand_kit_layout_selections_get",
|
|
38
|
-
"save_layout_selections_brand_kit_layout_selections_put",
|
|
39
|
-
"list_seo_audits_seo_audits_get",
|
|
40
|
-
"start_seo_audit_seo_audits_post",
|
|
41
|
-
"get_seo_audit_seo_audits__audit_id__get",
|
|
42
27
|
"get_seo_audit_issues_seo_audits__audit_id__issues_get",
|
|
28
|
+
"get_seo_audit_seo_audits__audit_id__get",
|
|
29
|
+
"list_archetypes_brand_kit_archetypes_get",
|
|
30
|
+
"list_content_pages_content_pages_get",
|
|
31
|
+
"list_landing_page_versions_landing_pages__landing_page_id__versions_get",
|
|
32
|
+
"list_landing_pages_landing_pages_get",
|
|
33
|
+
"list_seo_audits_seo_audits_get",
|
|
43
34
|
"list_seo_tracked_keywords_seo_rank_tracking_get",
|
|
35
|
+
"publish_landing_page_landing_pages__landing_page_id__publish_post",
|
|
36
|
+
"remove_seo_tracked_keywords_seo_rank_tracking_keywords_remove_post",
|
|
37
|
+
"restore_landing_page_version_landing_pages__landing_page_id__versions__version_num__restore_post",
|
|
38
|
+
"save_layout_selections_brand_kit_layout_selections_put",
|
|
44
39
|
"save_seo_tracked_keywords_seo_rank_tracking_keywords_post",
|
|
45
|
-
"
|
|
40
|
+
"start_seo_audit_seo_audits_post",
|
|
41
|
+
"trigger_brand_extraction_brand_kit_extract_post",
|
|
42
|
+
"unpublish_landing_page_landing_pages__landing_page_id__unpublish_post",
|
|
43
|
+
"update_content_page_content_pages__page_id__patch",
|
|
44
|
+
"update_landing_page_landing_pages__landing_page_id__patch",
|
|
45
|
+
"update_landing_page_section_landing_pages__landing_page_id__sections__section_idx__patch"
|
|
46
46
|
],
|
|
47
47
|
"components": [
|
|
48
|
+
{
|
|
49
|
+
"name": "PagesScreen",
|
|
50
|
+
"file": "components/pages-screen.tsx",
|
|
51
|
+
"purpose": "Finished marketing-site workspace: landing pages with beside-the-list editor, content library, brand kit and SEO tabs.",
|
|
52
|
+
"operations": [
|
|
53
|
+
"get_landing_page_landing_pages__landing_page_id__get",
|
|
54
|
+
"list_landing_pages_landing_pages_get"
|
|
55
|
+
],
|
|
56
|
+
"tiers": [
|
|
57
|
+
"read"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
48
60
|
{
|
|
49
61
|
"name": "PageList",
|
|
50
62
|
"file": "components/page-list.tsx",
|
|
@@ -218,6 +230,7 @@
|
|
|
218
230
|
}
|
|
219
231
|
],
|
|
220
232
|
"peerDependencies": {
|
|
221
|
-
"react": ">=18"
|
|
233
|
+
"react": ">=18",
|
|
234
|
+
"lucide-react": ">=0.400.0"
|
|
222
235
|
}
|
|
223
236
|
}
|
|
@@ -99,7 +99,9 @@ function Layouts() {
|
|
|
99
99
|
const picked = edit ?? saved;
|
|
100
100
|
const save = useG8Mutation(() => g8Put("/brand-kit/layout-selections", { archetype_ids: picked }), { onSuccess: () => { setEdit(null); void selections.refetch(); } });
|
|
101
101
|
const rows = rowsOf<Rec>(archetypes.data?.data);
|
|
102
|
-
|
|
102
|
+
// The saved selection must be known before anything is ticked or saved: ticking first would replace it with just that box.
|
|
103
|
+
const savedKnown = selections.data !== undefined && !selections.error;
|
|
104
|
+
const dirty = savedKnown && edit !== null && JSON.stringify([...picked].sort()) !== JSON.stringify([...saved].sort());
|
|
103
105
|
const toggle = (id: string) => setEdit(picked.includes(id) ? picked.filter((p) => p !== id) : [...picked, id]);
|
|
104
106
|
return (
|
|
105
107
|
<Card>
|
|
@@ -107,13 +109,14 @@ function Layouts() {
|
|
|
107
109
|
<CardContent className="space-y-3">
|
|
108
110
|
<p className="text-muted-foreground text-sm">New landing pages are built from the layouts you tick.</p>
|
|
109
111
|
{archetypes.error ? <ApiError error={archetypes.error} onRetry={() => void archetypes.refetch()} /> : null}
|
|
112
|
+
{selections.error ? <ApiError error={selections.error} title="Could not load your saved layouts" onRetry={() => void selections.refetch()} /> : null}
|
|
110
113
|
{archetypes.loading && !archetypes.data ? <Skeleton className="h-24 w-full" /> : null}
|
|
111
114
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
112
115
|
{rows.map((a) => {
|
|
113
116
|
const id = text(a.id);
|
|
114
117
|
return (
|
|
115
118
|
<label key={id} className="flex cursor-pointer items-start gap-2 rounded-md border p-3 text-sm">
|
|
116
|
-
<input type="checkbox" checked={picked.includes(id)} onChange={() => toggle(id)} aria-label={text(a.name)} />
|
|
119
|
+
<input type="checkbox" checked={picked.includes(id)} disabled={!savedKnown} onChange={() => toggle(id)} aria-label={text(a.name)} />
|
|
117
120
|
<span><span className="font-medium">{text(a.name)}</span><br /><span className="text-muted-foreground">{text(a.description)}</span></span>
|
|
118
121
|
</label>
|
|
119
122
|
);
|
|
@@ -23,12 +23,16 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
23
23
|
import { ApiError } from "@/g8/components/api-error";
|
|
24
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
25
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
26
|
-
import { asRec, num, rowsOf, text } from "../lib/studio";
|
|
27
|
-
import type { ContentPageRow } from "../lib/studio";
|
|
26
|
+
import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
|
|
27
|
+
import type { ContentPageRow, Rec } from "../lib/studio";
|
|
28
|
+
import { plural } from "@/g8/lib/plural";
|
|
28
29
|
|
|
29
30
|
export interface ContentLibraryProps {
|
|
30
31
|
pageSize?: number;
|
|
31
32
|
className?: string;
|
|
33
|
+
/** Controlled: the content item whose full record is open (for a `?id=` deep link). */
|
|
34
|
+
selectedId?: string | null;
|
|
35
|
+
onSelectedChange?: (id: string | null) => void;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const TYPES = ["", "website", "blog", "linkedin", "newsletter"];
|
|
@@ -45,14 +49,32 @@ function Select({ id, value, options, onChange }: { id: string; value: string; o
|
|
|
45
49
|
);
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
/** Every read-only field the content record carries (labels and keys come from the Studio content response). */
|
|
53
|
+
function contentFacts(d: Rec): Array<[string, string]> {
|
|
54
|
+
const n = (k: string): string => (d[k] == null ? "" : text(d[k]));
|
|
55
|
+
const yes = (k: string): string => (d[k] == null ? "" : d[k] ? "Yes" : "No");
|
|
56
|
+
return [
|
|
57
|
+
["Type", label(text(d.content_type, "website"))], ["Source", n("source")], ["Domain", n("domain")], ["Owned", yes("is_owned")],
|
|
58
|
+
["H1", n("h1")], ["Meta description", n("meta_description")], ["Words", n("word_count")], ["Crawl status", n("crawl_status")],
|
|
59
|
+
["SEO score", n("seo_score")], ["AEO score", n("aeo_score")], ["Freshness", n("freshness_score")], ["Priority", n("priority_score")],
|
|
60
|
+
["Authority", n("authority_score")], ["Backlinks", n("backlinks_total")], ["Referring domains", n("referring_domains_count")], ["Organic keywords", n("organic_keywords_count")],
|
|
61
|
+
["Keyword position", n("primary_keyword_position")], ["Keyword volume", n("primary_keyword_volume")], ["LLM grade", n("llm_grade")], ["Cited by LLMs", yes("llm_cited")],
|
|
62
|
+
["AI overview", yes("ai_overview_present")], ["Clicks", n("gsc_clicks")], ["Impressions", n("gsc_impressions")], ["CTR", n("gsc_ctr")], ["Search position", n("gsc_position")],
|
|
63
|
+
["Published", shortDate(d.published_at)], ["Last crawled", shortDate(d.last_crawled_at)], ["Created", shortDate(d.created_at)], ["Last synced", shortDate(d.semrush_last_synced)],
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function EditSheet({ pageId, onClose, onSaved, onAction }: { pageId: string; onClose: () => void; onSaved: () => void; onAction: (kind: "enrich" | "delete", page: ContentPageRow) => void }) {
|
|
49
68
|
const detail = useG8Query((signal) => g8Get(`/content/pages/${encodeURIComponent(pageId)}`, undefined, { signal }), [pageId]);
|
|
50
69
|
const [form, setForm] = useState({ title: "", primary_keyword: "", content_status: "idea", body_content: "" });
|
|
51
70
|
const d = asRec(detail.data?.data);
|
|
52
71
|
useEffect(() => {
|
|
53
72
|
setForm({ title: text(d.title), primary_keyword: text(d.primary_keyword), content_status: text(d.content_status, "idea"), body_content: text(d.body_content) });
|
|
54
73
|
}, [detail.data]);
|
|
55
|
-
|
|
74
|
+
// Only the fields the person changed are sent, so nothing they did not touch can be blanked.
|
|
75
|
+
const original = { title: text(d.title), primary_keyword: text(d.primary_keyword), content_status: text(d.content_status, "idea"), body_content: text(d.body_content) };
|
|
76
|
+
const changed = Object.fromEntries(Object.entries(form).filter(([k, v]) => v !== original[k as keyof typeof original]));
|
|
77
|
+
const save = useG8Mutation(() => g8Patch(`/content/pages/${encodeURIComponent(pageId)}`, changed), { onSuccess: () => { onSaved(); onClose(); } });
|
|
56
78
|
return (
|
|
57
79
|
<Sheet open onOpenChange={(open) => { if (!open) onClose(); }}>
|
|
58
80
|
<SheetContent className="w-full overflow-y-auto sm:max-w-xl">
|
|
@@ -62,14 +84,24 @@ function EditSheet({ pageId, onClose, onSaved }: { pageId: string; onClose: () =
|
|
|
62
84
|
</SheetHeader>
|
|
63
85
|
<div className="space-y-3 px-4 pb-4">
|
|
64
86
|
{detail.error ? <ApiError error={detail.error} onRetry={() => void detail.refetch()} /> : null}
|
|
65
|
-
{
|
|
87
|
+
{/* The form (and Save) exist only once the page has loaded: after a failed load there is nothing to edit over. */}
|
|
88
|
+
{!detail.data ? (detail.error ? null : <Skeleton className="h-32 w-full" />) : (
|
|
66
89
|
<>
|
|
90
|
+
<dl className="grid grid-cols-2 gap-2 text-sm">
|
|
91
|
+
{contentFacts(d).map(([k, v]) => (
|
|
92
|
+
<div key={k}><dt className="text-muted-foreground text-xs">{k}</dt><dd className="truncate" title={v}>{v || "Not set"}</dd></div>
|
|
93
|
+
))}
|
|
94
|
+
</dl>
|
|
95
|
+
<div className="flex gap-2">
|
|
96
|
+
<Button size="sm" variant="outline" onClick={() => onAction("enrich", { id: pageId, url: text(d.url), title: text(d.title) })}>Enrich SEO</Button>
|
|
97
|
+
<Button size="sm" variant="ghost" onClick={() => onAction("delete", { id: pageId, url: text(d.url), title: text(d.title) })}>Delete page</Button>
|
|
98
|
+
</div>
|
|
67
99
|
<div className="space-y-1"><Label htmlFor="cl-title">Title</Label><Input id="cl-title" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></div>
|
|
68
100
|
<div className="space-y-1"><Label htmlFor="cl-kw">Primary keyword</Label><Input id="cl-kw" value={form.primary_keyword} onChange={(e) => setForm({ ...form, primary_keyword: e.target.value })} /></div>
|
|
69
101
|
<div className="space-y-1"><Label htmlFor="cl-status">Status</Label><Select id="cl-status" value={form.content_status} options={STATUSES.slice(1)} onChange={(v) => setForm({ ...form, content_status: v })} /></div>
|
|
70
102
|
<div className="space-y-1"><Label htmlFor="cl-body">Body</Label><Textarea id="cl-body" rows={10} value={form.body_content} onChange={(e) => setForm({ ...form, body_content: e.target.value })} /></div>
|
|
71
103
|
{save.error ? <ApiError error={save.error} title="Could not save" /> : null}
|
|
72
|
-
<Button loading={save.loading} onClick={() => void save.mutate(undefined)}>Save</Button>
|
|
104
|
+
<Button loading={save.loading} disabled={Object.keys(changed).length === 0} onClick={() => void save.mutate(undefined)}>Save</Button>
|
|
73
105
|
</>
|
|
74
106
|
)}
|
|
75
107
|
</div>
|
|
@@ -78,12 +110,15 @@ function EditSheet({ pageId, onClose, onSaved }: { pageId: string; onClose: () =
|
|
|
78
110
|
);
|
|
79
111
|
}
|
|
80
112
|
|
|
81
|
-
export function ContentLibrary({ pageSize = 25, className }: ContentLibraryProps) {
|
|
113
|
+
export function ContentLibrary({ pageSize = 25, className, selectedId, onSelectedChange }: ContentLibraryProps) {
|
|
82
114
|
const [type, setType] = useState("");
|
|
83
115
|
const [status, setStatus] = useState("");
|
|
84
116
|
const [offset, setOffset] = useState(0);
|
|
85
117
|
const [url, setUrl] = useState("");
|
|
86
|
-
const [
|
|
118
|
+
const [localEditing, setLocalEditing] = useState<string | null>(null);
|
|
119
|
+
const controlled = onSelectedChange !== undefined;
|
|
120
|
+
const editing = controlled ? (selectedId ?? null) : localEditing;
|
|
121
|
+
const setEditing = (id: string | null): void => (controlled ? onSelectedChange?.(id) : setLocalEditing(id));
|
|
87
122
|
const [action, setAction] = useState<Action | null>(null);
|
|
88
123
|
|
|
89
124
|
const list = useG8Query(
|
|
@@ -93,7 +128,7 @@ export function ContentLibrary({ pageSize = 25, className }: ContentLibraryProps
|
|
|
93
128
|
const add = useG8Mutation((u: string) => g8Post("/content/pages", { url: u, content_type: type || "website" }), { onSuccess: () => { setUrl(""); void list.refetch(); } });
|
|
94
129
|
const run = useG8Mutation(
|
|
95
130
|
(a: Action) => (a.kind === "enrich" ? g8Post(`/content/pages/${encodeURIComponent(a.page.id)}/enrich-seo`) : g8Delete(`/content/pages/${encodeURIComponent(a.page.id)}`)),
|
|
96
|
-
{ onSuccess: () => { setAction(null); void list.refetch(); } },
|
|
131
|
+
{ onSuccess: (_r, a) => { setAction(null); if (a.kind === "delete" && a.page.id === editing) setEditing(null); void list.refetch(); } },
|
|
97
132
|
);
|
|
98
133
|
|
|
99
134
|
const data = asRec(list.data?.data);
|
|
@@ -143,14 +178,14 @@ export function ContentLibrary({ pageSize = 25, className }: ContentLibraryProps
|
|
|
143
178
|
</Table>
|
|
144
179
|
)}
|
|
145
180
|
<div className="mt-3 flex items-center justify-between text-sm">
|
|
146
|
-
<span className="text-muted-foreground">{total
|
|
181
|
+
<span className="text-muted-foreground">{plural(total, "item")}</span>
|
|
147
182
|
<div className="flex gap-2">
|
|
148
183
|
<Button size="sm" variant="outline" disabled={offset === 0} onClick={() => setOffset(Math.max(0, offset - pageSize))}>Previous</Button>
|
|
149
184
|
<Button size="sm" variant="outline" disabled={offset + pageSize >= total} onClick={() => setOffset(offset + pageSize)}>Next</Button>
|
|
150
185
|
</div>
|
|
151
186
|
</div>
|
|
152
187
|
|
|
153
|
-
{editing ? <EditSheet pageId={editing} onClose={() => setEditing(null)} onSaved={() => void list.refetch()} /> : null}
|
|
188
|
+
{editing ? <EditSheet pageId={editing} onClose={() => setEditing(null)} onSaved={() => void list.refetch()} onAction={(kind, page) => setAction({ kind, page })} /> : null}
|
|
154
189
|
{action ? (
|
|
155
190
|
<ConfirmSpendDialog open tier={action.kind === "enrich" ? "billable" : "destructive"}
|
|
156
191
|
title={action.kind === "enrich" ? `Enrich SEO for "${name(action.page)}"` : `Delete "${name(action.page)}"`}
|
|
@@ -25,10 +25,13 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
|
25
25
|
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
26
26
|
import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
|
|
27
27
|
import type { LandingPageRow } from "../lib/studio";
|
|
28
|
+
import { plural } from "@/g8/lib/plural";
|
|
28
29
|
|
|
29
30
|
export interface PageListProps {
|
|
30
31
|
/** Called with the page id when a row is opened. Wire it to your PageEditor route. */
|
|
31
32
|
onOpen?: (pageId: string) => void;
|
|
33
|
+
/** Called with the page id after a page is deleted (for example to close it if it is open beside the list). */
|
|
34
|
+
onDeleted?: (pageId: string) => void;
|
|
32
35
|
pageSize?: number;
|
|
33
36
|
className?: string;
|
|
34
37
|
}
|
|
@@ -56,7 +59,7 @@ function confirmCopy(p: Pending): { tier: ConfirmTier; title: string; descriptio
|
|
|
56
59
|
return { tier: "destructive", title: `Delete "${p.page.name}"`, description: "The page and its version history are removed.", label: "Delete", affected: 1 };
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
export function PageList({ onOpen, pageSize = 25, className }: PageListProps) {
|
|
62
|
+
export function PageList({ onOpen, onDeleted, pageSize = 25, className }: PageListProps) {
|
|
60
63
|
const [status, setStatus] = useState("");
|
|
61
64
|
const [offset, setOffset] = useState(0);
|
|
62
65
|
const [name, setName] = useState("");
|
|
@@ -72,7 +75,7 @@ export function PageList({ onOpen, pageSize = 25, className }: PageListProps) {
|
|
|
72
75
|
if (p.kind === "publish") return g8Post(`${base}/publish`);
|
|
73
76
|
if (p.kind === "unpublish") return g8Post(`${base}/unpublish`);
|
|
74
77
|
return g8Delete(base);
|
|
75
|
-
}, { onSuccess: () => { setPending(null); void list.refetch(); } });
|
|
78
|
+
}, { onSuccess: (_res, p) => { setPending(null); void list.refetch(); if (p.kind === "delete") onDeleted?.(p.page.id); } });
|
|
76
79
|
const blank = useG8Mutation((title: string) => g8Post("/landing-pages", { name: title || undefined, generate: false }), {
|
|
77
80
|
onSuccess: (res) => {
|
|
78
81
|
setName("");
|
|
@@ -143,7 +146,7 @@ export function PageList({ onOpen, pageSize = 25, className }: PageListProps) {
|
|
|
143
146
|
)}
|
|
144
147
|
|
|
145
148
|
<div className="mt-3 flex items-center justify-between text-sm">
|
|
146
|
-
<span className="text-muted-foreground">{total
|
|
149
|
+
<span className="text-muted-foreground">{plural(total, "page")}</span>
|
|
147
150
|
<div className="flex gap-2">
|
|
148
151
|
<Button size="sm" variant="outline" disabled={offset === 0} onClick={() => setOffset(Math.max(0, offset - pageSize))}>Previous</Button>
|
|
149
152
|
<Button size="sm" variant="outline" disabled={offset + pageSize >= total} onClick={() => setOffset(offset + pageSize)}>Next</Button>
|