@flamingo-stack/openframe-frontend-core 0.0.756 → 0.0.758
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/dist/build/export-map.json +1 -1
- package/dist/{chunk-7KPU7RGS.cjs → chunk-2AEGJHW2.cjs} +83 -82
- package/dist/{chunk-QKFJ6ORK.cjs → chunk-3C4U4UW6.cjs} +1 -1
- package/dist/{chunk-57JA37HP.cjs → chunk-5THYJU3G.cjs} +123 -77
- package/dist/{chunk-4YL4Y3BV.cjs → chunk-ECHJO4NI.cjs} +48 -24
- package/dist/{chunk-TAEEGNFL.cjs → chunk-GU3M4YF3.cjs} +75 -68
- package/dist/{chunk-MDXZMTCK.cjs → chunk-IQFXJSAH.cjs} +22 -22
- package/dist/{chunk-KNNJ4WLF.cjs → chunk-JEIAB7JQ.cjs} +2 -2
- package/dist/{chunk-MBQTLZAG.cjs → chunk-L2RNZOCI.cjs} +11 -11
- package/dist/{chunk-VOXDBCBV.cjs → chunk-N2US7DLB.cjs} +47 -36
- package/dist/{chunk-KFUC2XF7.cjs → chunk-RPADJ2E6.cjs} +3 -3
- package/dist/{chunk-PUK27ZB6.cjs → chunk-RUMRBL5X.cjs} +13 -13
- package/dist/{chunk-CYDWQYIQ.cjs → chunk-SEPHTWFL.cjs} +5 -5
- package/dist/{chunk-W5MEHJRQ.cjs → chunk-U2UVB4IF.cjs} +2 -2
- package/dist/{chunk-BTIYXBA2.cjs → chunk-WSRMYHIV.cjs} +2 -2
- package/dist/{chunk-KSU3CXKY.cjs → chunk-YBOA5GZV.cjs} +3 -3
- package/dist/{chunk-MR5KMPSE.cjs → chunk-YLVTECAD.cjs} +4 -4
- package/dist/{cloud-tenants-NOZGXQZY.cjs → cloud-tenants-WJCL4P3Y.cjs} +3 -3
- package/dist/components/case-studies/index.cjs +11 -11
- package/dist/components/chat/embeddable-chat.js +8 -6
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/docs/doc-viewer.d.ts +4 -0
- package/dist/components/docs/doc-viewer.d.ts.map +1 -1
- package/dist/components/docs/doc-viewer.js +3 -0
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/use-document-tree.d.ts.map +1 -1
- package/dist/components/docs/use-document-tree.js +9 -1
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/help-center-pages/index.cjs +57 -56
- package/dist/components/help-center-pages/trust-center-page.d.ts +7 -1
- package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/trust-center-page.js +4 -3
- package/dist/components/index.cjs +34 -32
- package/dist/components/layout/page-data-sync.d.ts +18 -0
- package/dist/components/layout/page-data-sync.d.ts.map +1 -0
- package/dist/components/layout/page-data-sync.js +33 -0
- package/dist/components/meeting-scheduler/index.cjs +19 -19
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/product-demo/index.cjs +7 -7
- package/dist/components/shared/delivery/delivery-lists.d.ts.map +1 -1
- package/dist/components/shared/delivery/delivery-lists.js +6 -0
- package/dist/components/shared/dev-section/dev-section-view.d.ts +3 -16
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-section-view.js +6 -13
- package/dist/components/shared/legal-document/legal-document-page.d.ts +1 -3
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.js +3 -9
- package/dist/components/shared/roadmap/roadmap-view.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-view.js +29 -17
- package/dist/components/tickets/hooks/use-tickets-list.d.ts +4 -0
- package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-tickets-list.js +1 -0
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/ticket-center.d.ts.map +1 -1
- package/dist/components/tickets/ticket-center.js +5 -6
- package/dist/components/ui/data-attribution.d.ts +13 -0
- package/dist/components/ui/data-attribution.d.ts.map +1 -1
- package/dist/components/ui/data-attribution.js +30 -1
- package/dist/components/ui/index.cjs +5 -3
- package/dist/{devices-M4KBTOTC.cjs → devices-PCMSALOA.cjs} +4 -4
- package/dist/{embeddable-chat-KGJVVG2V.cjs → embeddable-chat-CHHWPGAY.cjs} +8 -6
- package/dist/index.cjs +13 -7
- package/dist/{knowledge-VVF6CJEG.cjs → knowledge-FVCXNAMW.cjs} +3 -3
- package/dist/{logs-7E6HBJ55.cjs → logs-IINFWMUF.cjs} +11 -11
- package/dist/{policies-UMZLJTBD.cjs → policies-P3ZT774G.cjs} +7 -7
- package/dist/{scripts-XVNAEYCH.cjs → scripts-KO4V2XO5.cjs} +8 -8
- package/dist/{software-update-U43ZFHMC.cjs → software-update-PSJUU363.cjs} +6 -6
- package/dist/types/data-sync.d.ts +22 -0
- package/dist/types/data-sync.d.ts.map +1 -0
- package/dist/types/data-sync.js +12 -0
- package/dist/types/doc-source.d.ts +4 -0
- package/dist/types/doc-source.d.ts.map +1 -1
- package/dist/types/index.cjs +12 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/package.json +1 -1
- package/src/components/chat/embeddable-chat.tsx +10 -6
- package/src/components/docs/doc-viewer.tsx +9 -0
- package/src/components/docs/use-document-tree.ts +10 -0
- package/src/components/help-center-pages/trust-center-page.tsx +10 -3
- package/src/components/layout/page-data-sync.tsx +60 -0
- package/src/components/shared/delivery/delivery-lists.tsx +12 -2
- package/src/components/shared/dev-section/dev-section-view.tsx +37 -56
- package/src/components/shared/legal-document/legal-document-page.tsx +5 -13
- package/src/components/shared/roadmap/roadmap-view.tsx +37 -20
- package/src/components/tickets/hooks/use-tickets-list.ts +7 -0
- package/src/components/tickets/ticket-center.tsx +7 -3
- package/src/components/ui/data-attribution.tsx +44 -0
- package/src/types/data-sync.ts +26 -0
- package/src/types/doc-source.ts +4 -0
- package/src/types/index.ts +1 -0
|
@@ -31,11 +31,14 @@
|
|
|
31
31
|
import { useEffect, useState } from 'react';
|
|
32
32
|
import { useSearchParams, useRouter, usePathname } from '../../../embed-shims';
|
|
33
33
|
import { useScrollToHash } from '../../../hooks/use-scroll-to-hash';
|
|
34
|
+
import type { DataSync } from '../../../types/data-sync';
|
|
34
35
|
import type { DeliveryItem, DeliveryResponse } from '../../../types/delivery';
|
|
35
36
|
import { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys';
|
|
36
37
|
import { contentFetch } from '../../../utils/embed-content-fetch';
|
|
37
38
|
import { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';
|
|
39
|
+
import { usePageDataSync } from '../../layout/page-data-sync';
|
|
38
40
|
import { ListEmptyState } from '../../list-empty-state';
|
|
41
|
+
import { DataSyncLine } from '../../ui/data-attribution';
|
|
39
42
|
import { LoadError } from '../../ui/error-state';
|
|
40
43
|
import { DeliveryTable } from './delivery-table';
|
|
41
44
|
|
|
@@ -47,13 +50,15 @@ const DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search;
|
|
|
47
50
|
const DEFAULT_TASK_TYPE_PARAM_KEY = DEV_SECTION_PARAM_KEYS.deliveryTaskType;
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
|
-
* Wire shape of ONE bucket endpoint — `{ items, count, pagination }`. Only
|
|
51
|
-
* `items`
|
|
53
|
+
* Wire shape of ONE bucket endpoint — `{ items, count, pagination, sync }`. Only
|
|
54
|
+
* `items` and `sync` are read here; `items` stays optional because an embedder's proxy is
|
|
52
55
|
* free to answer with an empty envelope, and the render treats "no items" and
|
|
53
56
|
* "empty items" identically.
|
|
54
57
|
*/
|
|
55
58
|
interface DeliveryBucketResponse {
|
|
56
59
|
items?: DeliveryItem[];
|
|
60
|
+
/** Where the tasks are mirrored from and when it last read them (both buckets read the same mirror). */
|
|
61
|
+
sync?: DataSync;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
export interface DeliveryListsProps {
|
|
@@ -84,6 +89,7 @@ export function DeliveryLists({
|
|
|
84
89
|
const [data, setData] = useState<DeliveryResponse | null>(null);
|
|
85
90
|
const [isLoading, setIsLoading] = useState(true);
|
|
86
91
|
const [error, setError] = useState<string | null>(null);
|
|
92
|
+
const [sync, setSync] = useState<DataSync | null>(null);
|
|
87
93
|
|
|
88
94
|
// Get filter state from URL
|
|
89
95
|
const searchQuery = searchParams.get(searchParamKey) || '';
|
|
@@ -134,6 +140,7 @@ export function DeliveryLists({
|
|
|
134
140
|
completed: completedResult.items ?? [],
|
|
135
141
|
inProgress: inProgressResult.items ?? [],
|
|
136
142
|
});
|
|
143
|
+
setSync(completedResult.sync ?? inProgressResult.sync ?? null);
|
|
137
144
|
} catch (err) {
|
|
138
145
|
if (cancelled) return;
|
|
139
146
|
console.error('Error fetching delivery items:', err);
|
|
@@ -160,6 +167,8 @@ export function DeliveryLists({
|
|
|
160
167
|
// by RoadmapView). 96 matches the sticky-header offset every
|
|
161
168
|
// hash-scroll surface in the app uses.
|
|
162
169
|
useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX });
|
|
170
|
+
// The page states the sync under its title; the lists shown without that page draw it above themselves.
|
|
171
|
+
const syncInPage = usePageDataSync(sync, isLoading && !data);
|
|
163
172
|
|
|
164
173
|
const showCompleted = true;
|
|
165
174
|
const showInProgress = true;
|
|
@@ -180,6 +189,7 @@ export function DeliveryLists({
|
|
|
180
189
|
|
|
181
190
|
return (
|
|
182
191
|
<div className="flex w-full flex-col gap-[40px]">
|
|
192
|
+
{!syncInPage && <DataSyncLine sync={sync} loading={isLoading && !data} />}
|
|
183
193
|
{/* Empty state if no results after filtering */}
|
|
184
194
|
{!isLoading && !hasResults && (
|
|
185
195
|
<ListEmptyState
|
|
@@ -23,26 +23,15 @@ import {
|
|
|
23
23
|
type OpenframeDevSectionKey,
|
|
24
24
|
} from '../../../utils/dev-sections/openframe-dev-sections';
|
|
25
25
|
import { StatusFilterComponent } from '../../features';
|
|
26
|
+
import { PageDataSyncProvider, PageDataSyncRow } from '../../layout/page-data-sync';
|
|
27
|
+
import { SectionHeading } from '../../layout/page-heading';
|
|
26
28
|
import { SearchInput } from '../../ui';
|
|
27
29
|
|
|
28
30
|
export interface DevSectionViewProps {
|
|
29
31
|
/** Which section to render — drives title, search, and filter
|
|
30
32
|
* config via the `OPENFRAME_DEV_SECTIONS` registry. */
|
|
31
33
|
sectionKey: OpenframeDevSectionKey;
|
|
32
|
-
/**
|
|
33
|
-
* Omit for the compact tab-context heading. */
|
|
34
|
-
hero?: {
|
|
35
|
-
/** Pre-rendered icon JSX. Server components render the icon themselves
|
|
36
|
-
* and pass the element here — function references can't cross the
|
|
37
|
-
* server→client boundary, but React elements can. */
|
|
38
|
-
icon: ReactNode;
|
|
39
|
-
/** Hero title. Falls back to `OPENFRAME_DEV_SECTIONS[sectionKey].hero.title`
|
|
40
|
-
* when omitted, so embedders can override the (OpenFrame-specific) default
|
|
41
|
-
* copy without forking the registry. */
|
|
42
|
-
title?: string;
|
|
43
|
-
description: string;
|
|
44
|
-
};
|
|
45
|
-
/** Optional slot rendered BETWEEN the hero and the search/filter
|
|
34
|
+
/** Optional slot rendered BETWEEN the heading and the search/filter
|
|
46
35
|
* controls. Use this for an entry-action surface that should sit
|
|
47
36
|
* above the list (e.g. the Help Center's "Open a new ticket" form).
|
|
48
37
|
* The slot is wrapped in the same `gap-10` flex column so spacing
|
|
@@ -52,7 +41,7 @@ export interface DevSectionViewProps {
|
|
|
52
41
|
/** The page-specific list body. Reads URL params written by this
|
|
53
42
|
* component (search input + filter pills). */
|
|
54
43
|
children: ReactNode;
|
|
55
|
-
/** Render this component's own title heading (
|
|
44
|
+
/** Render this component's own title heading (the section's `h2`). Default
|
|
56
45
|
* `true` — the tab context renders the compact heading itself. Pass `false`
|
|
57
46
|
* when the host already renders the title elsewhere (e.g. `DevSectionPage`
|
|
58
47
|
* routes it through the unified `PageLayout` `TitleBlock`): then only the
|
|
@@ -60,7 +49,7 @@ export interface DevSectionViewProps {
|
|
|
60
49
|
showHeading?: boolean;
|
|
61
50
|
}
|
|
62
51
|
|
|
63
|
-
export function DevSectionView({ sectionKey,
|
|
52
|
+
export function DevSectionView({ sectionKey, preControls, children, showHeading = true }: DevSectionViewProps) {
|
|
64
53
|
const section = OPENFRAME_DEV_SECTIONS[sectionKey];
|
|
65
54
|
const router = useRouter();
|
|
66
55
|
const pathname = usePathname();
|
|
@@ -123,50 +112,42 @@ export function DevSectionView({ sectionKey, hero, preControls, children, showHe
|
|
|
123
112
|
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
124
113
|
};
|
|
125
114
|
|
|
115
|
+
// The list reports where its records are mirrored from; the line states it at the end of the section's heading row
|
|
116
|
+
// (under the page's title when this view shows no heading of its own).
|
|
126
117
|
return (
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
<h1 className="flex items-center gap-3 tracking-[-1.12px] text-ods-text-primary text-h1">
|
|
132
|
-
{hero.icon}
|
|
133
|
-
{hero.title ?? section.hero.title}
|
|
134
|
-
</h1>
|
|
135
|
-
<p className="max-w-3xl text-ods-text-secondary text-h4">{hero.description}</p>
|
|
136
|
-
</div>
|
|
118
|
+
<PageDataSyncProvider>
|
|
119
|
+
<div className="flex w-full flex-col gap-10">
|
|
120
|
+
{showHeading ? (
|
|
121
|
+
<SectionHeading title={section.hero.title} punctuation=":" action={<PageDataSyncRow inline />} />
|
|
137
122
|
) : (
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
{section.hero.title}
|
|
141
|
-
<span className="text-ods-accent">:</span>
|
|
142
|
-
</h2>
|
|
143
|
-
</div>
|
|
144
|
-
))}
|
|
123
|
+
<PageDataSyncRow />
|
|
124
|
+
)}
|
|
145
125
|
|
|
146
|
-
|
|
126
|
+
{preControls}
|
|
147
127
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
128
|
+
{(search || filter) && (
|
|
129
|
+
<div className="space-y-4">
|
|
130
|
+
{search && (
|
|
131
|
+
<SearchInput
|
|
132
|
+
showDropdown={false}
|
|
133
|
+
placeholder={search.placeholder}
|
|
134
|
+
value={searchValue}
|
|
135
|
+
onChange={handleSearchChange}
|
|
136
|
+
onSubmit={handleSearchSubmit}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
{filter && (
|
|
140
|
+
<StatusFilterComponent
|
|
141
|
+
selectedStatus={currentFilterValue}
|
|
142
|
+
onStatusChange={handleFilterChange}
|
|
143
|
+
statusOptions={[...filter.options]}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
168
148
|
|
|
169
|
-
|
|
170
|
-
|
|
149
|
+
{children}
|
|
150
|
+
</div>
|
|
151
|
+
</PageDataSyncProvider>
|
|
171
152
|
);
|
|
172
153
|
}
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
import type { ComponentType } from 'react';
|
|
21
21
|
import Link from '../../../embed-shims/next-link';
|
|
22
22
|
import { useRouter } from '../../../embed-shims/next-navigation';
|
|
23
|
-
import { formatLegalDate } from '../../../utils/format';
|
|
24
23
|
import { FaqSection, pageFaqProps, type PageFaqSlot } from '../../faq/faq-section';
|
|
25
24
|
import { UnifiedSkeleton } from '../../loading/unified-skeleton';
|
|
26
25
|
import { PageShell, PageLayout } from '../../ui';
|
|
26
|
+
import { DataSyncLine } from '../../ui/data-attribution';
|
|
27
27
|
import { RichMarkdownRenderer } from '../../ui/markdown';
|
|
28
28
|
import { useLegalDocs, type LegalDocument } from './use-legal-docs';
|
|
29
29
|
import { useLegalDocuments } from './use-legal-documents';
|
|
@@ -56,8 +56,6 @@ export interface LegalDocumentPageProps {
|
|
|
56
56
|
emptyStateMessage?: string;
|
|
57
57
|
/** SSR-prepared document, if available. */
|
|
58
58
|
initialData?: LegalDocument | null;
|
|
59
|
-
/** SSR-prepared formatted "Last Updated" label. Stable across hydration. */
|
|
60
|
-
initialLastUpdatedLabel?: string | null;
|
|
61
59
|
/** Override the default `/api/legal/<docType>` endpoint
|
|
62
60
|
* (reverse-proxy embedders, alternate API paths). */
|
|
63
61
|
apiEndpoint?: string;
|
|
@@ -109,7 +107,6 @@ export function LegalDocumentPage({
|
|
|
109
107
|
errorTitle: errorTitleProp,
|
|
110
108
|
emptyStateMessage: emptyStateMessageProp,
|
|
111
109
|
initialData = null,
|
|
112
|
-
initialLastUpdatedLabel = null,
|
|
113
110
|
apiEndpoint,
|
|
114
111
|
MarkdownRenderer = RichMarkdownRenderer,
|
|
115
112
|
documentsEndpoint,
|
|
@@ -139,21 +136,16 @@ export function LegalDocumentPage({
|
|
|
139
136
|
const errorTitle = errorTitleProp ?? `Unable to load ${title}`;
|
|
140
137
|
const emptyStateMessage = emptyStateMessageProp ?? `${title} is not available at this time.`;
|
|
141
138
|
|
|
142
|
-
const fallbackLastUpdatedLabel = data?.lastSynced != null ? formatLegalDate(data.lastSynced) : null;
|
|
143
|
-
const effectiveLastUpdatedLabel = initialLastUpdatedLabel ?? fallbackLastUpdatedLabel;
|
|
144
|
-
|
|
145
139
|
// Subtitle routes through the frozen `PageLayout` `TitleBlock` (text-h2 title
|
|
146
140
|
// + subtitle) — unified header across all help-center pages. A document that
|
|
147
141
|
// states its own version and effective date shows those (they are what a
|
|
148
|
-
// reader agreed to)
|
|
149
|
-
//
|
|
150
|
-
const subtitle =
|
|
151
|
-
legalDocumentStampLabel(data) ??
|
|
152
|
-
(effectiveLastUpdatedLabel ? `Last Updated: ${effectiveLastUpdatedLabel}` : fallbackDescription);
|
|
142
|
+
// reader agreed to), else the fallback description. When it was last read from
|
|
143
|
+
// its repository is the shared sync line under the title.
|
|
144
|
+
const subtitle = legalDocumentStampLabel(data) ?? fallbackDescription;
|
|
153
145
|
|
|
154
146
|
const inner = (
|
|
155
147
|
<PageLayout title={title} subtitle={subtitle} backButton={backCfg} titleSize="h1" titleWrap>
|
|
156
|
-
{data
|
|
148
|
+
<DataSyncLine sync={data ? { source: 'github', syncedAt: data.lastSynced } : null} loading={isLoading && !data} />
|
|
157
149
|
|
|
158
150
|
<div className="flex flex-1 flex-col items-start gap-6 lg:flex-row lg:gap-10">
|
|
159
151
|
<div className="flex-1">
|
|
@@ -15,15 +15,20 @@ import { useMemo } from 'react';
|
|
|
15
15
|
import { useSearchParams } from '../../../embed-shims';
|
|
16
16
|
import { useScrollToHash } from '../../../hooks/use-scroll-to-hash';
|
|
17
17
|
import { useSelfFetch } from '../../../hooks/use-self-fetch';
|
|
18
|
+
import type { DataSync } from '../../../types/data-sync';
|
|
18
19
|
import { DEV_SECTION_PARAM_KEYS } from '../../../utils/dev-sections/dev-section-param-keys';
|
|
19
20
|
import { STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav';
|
|
20
21
|
import type { RoadmapItem } from '../../chat/types/entities/roadmap-item';
|
|
22
|
+
import { usePageDataSync } from '../../layout/page-data-sync';
|
|
23
|
+
import { DataSyncLine } from '../../ui/data-attribution';
|
|
21
24
|
import { LoadError } from '../../ui/error-state';
|
|
22
25
|
import { RoadmapGrid } from './roadmap-grid';
|
|
23
26
|
import { RoadmapGridSkeleton } from './roadmap-grid-skeleton';
|
|
24
27
|
import type { UseRoadmapVotingOptions } from './use-roadmap-voting';
|
|
25
28
|
|
|
26
29
|
const DEFAULT_ENDPOINT = '/api/roadmap';
|
|
30
|
+
/** The list under its sync line (the line the endpoint's `sync` states). */
|
|
31
|
+
const SYNCED_LIST_CLASS = 'flex w-full flex-col gap-[var(--spacing-system-lf)]';
|
|
27
32
|
// Defaults sourced from the ONE param-key registry the chrome (OPENFRAME_DEV_SECTIONS) also
|
|
28
33
|
// reads, so the chrome's written `?key=` and this view's read can't silently diverge.
|
|
29
34
|
const DEFAULT_SEARCH_PARAM_KEY = DEV_SECTION_PARAM_KEYS.search;
|
|
@@ -73,7 +78,9 @@ export function RoadmapView({
|
|
|
73
78
|
// hook's initialData re-sync effect fires every render and clobbers the
|
|
74
79
|
// optimistic vote patch below.
|
|
75
80
|
const initialData = useMemo(() => (initialItems ? { items: initialItems } : undefined), [initialItems]);
|
|
76
|
-
const { data, setData, isLoading, error, reload } = useSelfFetch<{ items?: RoadmapItem[] }>(url, {
|
|
81
|
+
const { data, setData, isLoading, error, reload } = useSelfFetch<{ items?: RoadmapItem[]; sync?: DataSync }>(url, {
|
|
82
|
+
initialData,
|
|
83
|
+
});
|
|
77
84
|
const items = data?.items ?? [];
|
|
78
85
|
|
|
79
86
|
// Deep-link hash dispatch — `?search=<id>#roadmap-<id>` from a chat card.
|
|
@@ -84,6 +91,8 @@ export function RoadmapView({
|
|
|
84
91
|
// them when `hasActiveFilters` is true (chat URL carries `?search=<id>`).
|
|
85
92
|
// The card mounts one tick after `data` lands; the hook waits.
|
|
86
93
|
useScrollToHash(data, { headerOffset: STICKY_HEADER_OFFSET_PX });
|
|
94
|
+
// The page states the sync under its title; a roadmap shown without that page draws it above the list.
|
|
95
|
+
const syncInPage = usePageDataSync(data?.sync, isLoading && !data);
|
|
87
96
|
|
|
88
97
|
if (error) {
|
|
89
98
|
return <LoadError message="Failed to load roadmap." onRetry={reload} />;
|
|
@@ -91,27 +100,35 @@ export function RoadmapView({
|
|
|
91
100
|
// Skeleton only while the FIRST fetch is in flight (no data yet) — a malformed
|
|
92
101
|
// body lacking `items` renders the grid (empty), never a stuck skeleton.
|
|
93
102
|
if (isLoading && !data) {
|
|
94
|
-
return
|
|
103
|
+
return (
|
|
104
|
+
<div className={SYNCED_LIST_CLASS}>
|
|
105
|
+
{!syncInPage && <DataSyncLine loading />}
|
|
106
|
+
<RoadmapGridSkeleton showLeftMargin={showLeftMargin} />
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
return (
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
<div className={SYNCED_LIST_CLASS}>
|
|
113
|
+
{!syncInPage && <DataSyncLine sync={data?.sync} />}
|
|
114
|
+
<RoadmapGrid
|
|
115
|
+
items={items}
|
|
116
|
+
showLeftMargin={showLeftMargin}
|
|
117
|
+
buildRefreshUrl={buildRefreshUrl}
|
|
118
|
+
votingOptions={votingOptions}
|
|
119
|
+
// Full-page roadmap → collapsible quarter grouping (the shared RoadmapGrid
|
|
120
|
+
// capability; flat grids stay the RoadmapGrid default). When the chrome's
|
|
121
|
+
// search / status filter is active, expand every quarter so matches aren't hidden.
|
|
122
|
+
groupByQuarter
|
|
123
|
+
hasActiveFilters={search !== '' || status !== 'all'}
|
|
124
|
+
// After a vote refreshes a single task, patch it into the fetched list so
|
|
125
|
+
// the displayed counts stay live.
|
|
126
|
+
onItemUpdate={updated =>
|
|
127
|
+
setData(prev =>
|
|
128
|
+
prev ? { ...prev, items: (prev.items ?? []).map(it => (it.id === updated.id ? updated : it)) } : prev,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
116
133
|
);
|
|
117
134
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { useQuery } from '@tanstack/react-query';
|
|
23
23
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context';
|
|
24
|
+
import type { DataSync } from '../../../types/data-sync';
|
|
24
25
|
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch';
|
|
25
26
|
import { PAGE_PARAM_LIMITS, pageCount, positiveInt } from '../../../utils/search-params';
|
|
26
27
|
import type { TicketData } from '../types';
|
|
@@ -36,6 +37,8 @@ interface FindTicketResponse {
|
|
|
36
37
|
pageSize?: number;
|
|
37
38
|
totalPages?: number;
|
|
38
39
|
scope?: 'self' | 'all';
|
|
40
|
+
/** Where the tickets are mirrored from and when it last read them. */
|
|
41
|
+
sync?: DataSync;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export interface UseTicketsListFilters {
|
|
@@ -71,6 +74,9 @@ export interface UseTicketsListReturn {
|
|
|
71
74
|
/** Wall-clock timestamp of the last successful fetch; null while
|
|
72
75
|
* loading the first time. */
|
|
73
76
|
lastUpdatedAt: number | null;
|
|
77
|
+
/** Where the tickets are mirrored from and when that mirror last read them (the endpoint's `sync`); null when the
|
|
78
|
+
* host states none. */
|
|
79
|
+
sync: DataSync | null;
|
|
74
80
|
/** Total ticket count across all pages (NOT just the current page).
|
|
75
81
|
* Drives the `<UnifiedPagination>` total-pages calculation. */
|
|
76
82
|
totalCount: number;
|
|
@@ -168,6 +174,7 @@ export function useTicketsList(filters: UseTicketsListFilters): UseTicketsListRe
|
|
|
168
174
|
void query.refetch();
|
|
169
175
|
},
|
|
170
176
|
lastUpdatedAt: query.dataUpdatedAt || null,
|
|
177
|
+
sync: data?.sync ?? null,
|
|
171
178
|
totalCount,
|
|
172
179
|
page: echoedPage,
|
|
173
180
|
pageSize: echoedPageSize,
|
|
@@ -19,14 +19,15 @@ import { useQueryClient } from '@tanstack/react-query';
|
|
|
19
19
|
import { RefreshCw } from 'lucide-react';
|
|
20
20
|
import { useCallback, useState } from 'react';
|
|
21
21
|
import { toast as defaultToast } from '../../hooks/use-toast';
|
|
22
|
-
import { formatRelativeTime } from '../../utils/date-utils';
|
|
23
22
|
import { devSectionAnchorId } from '../../utils/dev-sections/dev-section-param-keys';
|
|
24
23
|
import { useChatIdentity } from '../chat/hooks/use-chat-identity';
|
|
25
24
|
import { EmptyState } from '../empty-state';
|
|
25
|
+
import { usePageDataSync } from '../layout/page-data-sync';
|
|
26
26
|
import { LoadingRegion } from '../loading/loading-region';
|
|
27
27
|
import { UnifiedSkeleton } from '../loading/unified-skeleton';
|
|
28
28
|
import { Button } from '../ui/button';
|
|
29
29
|
import { Card } from '../ui/card';
|
|
30
|
+
import { DataSyncLine } from '../ui/data-attribution';
|
|
30
31
|
import { useTicketActions } from './hooks/use-ticket-actions';
|
|
31
32
|
import { useTicketsList } from './hooks/use-tickets-list';
|
|
32
33
|
import { TicketOpenForm } from './ticket-open-form';
|
|
@@ -71,7 +72,7 @@ function TicketCenterAuthed({
|
|
|
71
72
|
sessionEmail: string;
|
|
72
73
|
}) {
|
|
73
74
|
const queryClient = useQueryClient();
|
|
74
|
-
const { tickets, isLoading, isFetching, refetch,
|
|
75
|
+
const { tickets, isLoading, isFetching, refetch, sync } = useTicketsList({
|
|
75
76
|
customerEmail: sessionEmail,
|
|
76
77
|
});
|
|
77
78
|
const [optimisticTickets, setOptimisticTickets] = useState<OptimisticTicket[]>([]);
|
|
@@ -117,6 +118,9 @@ function TicketCenterAuthed({
|
|
|
117
118
|
setExpandedTicketId(prev => (prev === id ? null : id));
|
|
118
119
|
}, []);
|
|
119
120
|
|
|
121
|
+
// The page states the sync under its title; the ticket list shown without that page draws it in its own header.
|
|
122
|
+
const syncInPage = usePageDataSync(sync, isLoading);
|
|
123
|
+
|
|
120
124
|
const merged: AnyTicket[] = [...optimisticTickets, ...tickets];
|
|
121
125
|
|
|
122
126
|
return (
|
|
@@ -131,7 +135,7 @@ function TicketCenterAuthed({
|
|
|
131
135
|
<div className="flex items-center justify-between gap-3">
|
|
132
136
|
<p className="text-ods-text-secondary text-h5">Your Current Tickets</p>
|
|
133
137
|
<div className="flex items-center gap-3 text-ods-text-secondary text-h6">
|
|
134
|
-
{
|
|
138
|
+
{!syncInPage && <DataSyncLine sync={sync} inline />}
|
|
135
139
|
<Button
|
|
136
140
|
type="button"
|
|
137
141
|
variant="transparent"
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import { DATA_SYNC_SOURCES, type DataSync, type DataSyncSource } from '../../types/data-sync';
|
|
2
3
|
import { formatDate } from '../../utils/format';
|
|
4
|
+
import { ClickupIcon } from '../icons-v2-generated/brand-logos/clickup-icon';
|
|
5
|
+
import { GithubClassicIcon } from '../icons-v2-generated/brand-logos/github-classic-icon';
|
|
6
|
+
import { HubspotIcon } from '../icons-v2-generated/brand-logos/hubspot-icon';
|
|
3
7
|
import { TextSkeleton, UnifiedSkeleton } from '../loading/unified-skeleton';
|
|
4
8
|
|
|
5
9
|
export interface DataAttributionProps {
|
|
@@ -61,3 +65,43 @@ export function DataAttributionSkeleton() {
|
|
|
61
65
|
</div>
|
|
62
66
|
);
|
|
63
67
|
}
|
|
68
|
+
|
|
69
|
+
/** The mark each sync source is drawn with (the app's standard brand marks); its name is `DATA_SYNC_SOURCES`'. */
|
|
70
|
+
const DATA_SYNC_ICONS: Record<DataSyncSource, typeof ClickupIcon> = {
|
|
71
|
+
clickup: ClickupIcon,
|
|
72
|
+
hubspot: HubspotIcon,
|
|
73
|
+
github: GithubClassicIcon,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* THE sync line of a shared page whose records an API reads from a mirror (the roadmap and the delivery board:
|
|
78
|
+
* ClickUp; the ticket center: HubSpot; the docs and the legal documents: GitHub). The page draws whatever `sync`
|
|
79
|
+
* its endpoint answers, so a host that states none shows no line. `loading` draws the line's skeleton, so the page
|
|
80
|
+
* does not move when it fills in. It sits on its own row, at the end; `inline` leaves it in the caller's row.
|
|
81
|
+
*/
|
|
82
|
+
export function DataSyncLine({
|
|
83
|
+
sync,
|
|
84
|
+
loading = false,
|
|
85
|
+
inline = false,
|
|
86
|
+
action,
|
|
87
|
+
}: {
|
|
88
|
+
sync?: DataSync | null;
|
|
89
|
+
loading?: boolean;
|
|
90
|
+
inline?: boolean;
|
|
91
|
+
action?: ReactNode;
|
|
92
|
+
}) {
|
|
93
|
+
if (!loading && !(sync && sync.source in DATA_SYNC_ICONS)) return null;
|
|
94
|
+
const Icon = sync ? DATA_SYNC_ICONS[sync.source] : null;
|
|
95
|
+
const line =
|
|
96
|
+
loading || !sync || !Icon ? (
|
|
97
|
+
<DataAttributionSkeleton />
|
|
98
|
+
) : (
|
|
99
|
+
<DataAttribution
|
|
100
|
+
icon={<Icon size={16} />}
|
|
101
|
+
source={DATA_SYNC_SOURCES[sync.source]}
|
|
102
|
+
lastUpdated={sync.syncedAt}
|
|
103
|
+
action={action}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
return inline ? line : <div className="flex w-full justify-end">{line}</div>;
|
|
107
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every system a shared page's records are mirrored from, with the name a page shows for it. THE list: `DataSync`,
|
|
3
|
+
* `DataSyncLine`'s marks, the docs hook and every host that answers a `sync` read it.
|
|
4
|
+
*/
|
|
5
|
+
export const DATA_SYNC_SOURCES = {
|
|
6
|
+
clickup: 'ClickUp',
|
|
7
|
+
hubspot: 'HubSpot',
|
|
8
|
+
github: 'GitHub',
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type DataSyncSource = keyof typeof DATA_SYNC_SOURCES;
|
|
12
|
+
|
|
13
|
+
export function isDataSyncSource(value: unknown): value is DataSyncSource {
|
|
14
|
+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(DATA_SYNC_SOURCES, value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a list's records are mirrored from and when that mirror last read them: the `sync` a shared page's
|
|
19
|
+
* endpoint answers beside its records, drawn by `DataSyncLine`. Server-safe.
|
|
20
|
+
*/
|
|
21
|
+
export interface DataSync {
|
|
22
|
+
/** The system the records are read from. */
|
|
23
|
+
source: DataSyncSource;
|
|
24
|
+
/** ISO instant of the mirror's last successful read, or null when it never ran. */
|
|
25
|
+
syncedAt: string | null;
|
|
26
|
+
}
|
package/src/types/doc-source.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DataSync } from './data-sync';
|
|
1
2
|
/**
|
|
2
3
|
* Single home for doc-source tree + content types.
|
|
3
4
|
*
|
|
@@ -55,6 +56,9 @@ export interface DocContent {
|
|
|
55
56
|
* from the underlying row's sync/update timestamp. Undefined when unknown. */
|
|
56
57
|
publishedAt?: string;
|
|
57
58
|
updatedAt?: string;
|
|
59
|
+
/** Where the document is mirrored from and when it was last read (a synced docs source only); the viewer draws
|
|
60
|
+
* it as the shared sync line. */
|
|
61
|
+
sync?: DataSync;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
package/src/types/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './announcement';
|
|
|
6
6
|
// Doc-source viewer types — DocNode / DocContent / DocSourceDal /
|
|
7
7
|
// DocRenderHandlers. Consumed by knowledge-base, data-room, and future
|
|
8
8
|
// doc-source viewers.
|
|
9
|
+
export * from './data-sync';
|
|
9
10
|
export * from './doc-source';
|
|
10
11
|
export type { Category, CategoryCardProps, RealCategoryCardProps } from './categories';
|
|
11
12
|
export * from './category';
|