@startsimpli/integrations 0.2.9 → 0.2.13
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/integrations",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "Provider-agnostic types, API clients, hooks, and UI primitives for email + calendar integrations across StartSimpli apps.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -367,6 +367,29 @@ describe('listCommitments speaks the tenant data API’s own filter language', (
|
|
|
367
367
|
expect(page.appliedFilters).not.toContain('owner');
|
|
368
368
|
});
|
|
369
369
|
|
|
370
|
+
it('believes the server through the CAMELCASE client too', async () => {
|
|
371
|
+
// `@startsimpli/api` converts every response key snake_case -> camelCase, so
|
|
372
|
+
// in a browser the tenant's `applied_filters` arrives as `appliedFilters`.
|
|
373
|
+
// Reading only one spelling put "this workspace's data API did not filter by
|
|
374
|
+
// status" on a deployed screen while the server was demonstrably filtering
|
|
375
|
+
// by status. Same lesson as accepting `document_id` in a citation.
|
|
376
|
+
const seen: Array<Record<string, unknown>> = [];
|
|
377
|
+
const api = createCorpusApi({
|
|
378
|
+
http: {
|
|
379
|
+
async get<T>(_path: string, options?: { params?: Record<string, unknown> }): Promise<T> {
|
|
380
|
+
seen.push(options?.params ?? {});
|
|
381
|
+
return { count: 0, next: null, previous: null, results: [],
|
|
382
|
+
appliedFilters: ['attr.status__in'], ignoredFilters: [] } as T;
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
scope: { orgId: 'tandem' },
|
|
386
|
+
supportedFilters: [],
|
|
387
|
+
});
|
|
388
|
+
const page = await api.listCommitments({ status: ['open', 'blocked'] });
|
|
389
|
+
expect(page.appliedFilters).toEqual(['status']);
|
|
390
|
+
expect(seen[0]['attr.status__in']).toBe('open,blocked');
|
|
391
|
+
});
|
|
392
|
+
|
|
370
393
|
it('falls back to the declared list on a server that does not report', async () => {
|
|
371
394
|
const { api } = client(undefined);
|
|
372
395
|
const page = await api.listCommitments({ status: 'open' });
|
|
@@ -247,6 +247,33 @@ describe('TrackerPage — what is outstanding', () => {
|
|
|
247
247
|
render(<TrackerPage api={makeApi({ rows: [] })} scope={SCOPE} now={NOW} />);
|
|
248
248
|
expect(await screen.findByText(/Nothing has been pulled out of your meetings yet/)).toBeInTheDocument();
|
|
249
249
|
});
|
|
250
|
+
|
|
251
|
+
it('does NOT say the workspace is empty when a VIEW is empty', async () => {
|
|
252
|
+
// Browser-verified failure on the deployed tenant: the Overdue tab said
|
|
253
|
+
// "Nothing has been pulled out of your meetings yet" over ninety-seven open
|
|
254
|
+
// items, because nothing carried a due date. Advice that is false is worse
|
|
255
|
+
// than no advice.
|
|
256
|
+
const rows = [commitment({ externalId: 'commit:x', title: 'An open item' })];
|
|
257
|
+
const api: CommitmentTrackerApi = {
|
|
258
|
+
async listCommitments(query: CommitmentQuery = {}) {
|
|
259
|
+
// The server filters: the overdue query matches nothing, an unfiltered
|
|
260
|
+
// count-only probe still reports the workspace has rows.
|
|
261
|
+
const filtered = Boolean(query.dueBefore);
|
|
262
|
+
return {
|
|
263
|
+
count: filtered ? 0 : rows.length,
|
|
264
|
+
next: null, previous: null,
|
|
265
|
+
results: filtered ? [] : rows,
|
|
266
|
+
appliedFilters: ['status', 'dueBefore'],
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
render(<TrackerPage api={api} scope={SCOPE} now={NOW} />);
|
|
271
|
+
await screen.findByText('An open item');
|
|
272
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Overdue' }));
|
|
273
|
+
|
|
274
|
+
expect(await screen.findByText(/Nothing matches this view/)).toBeInTheDocument();
|
|
275
|
+
expect(screen.queryByText(/Nothing has been pulled out of your meetings yet/)).not.toBeInTheDocument();
|
|
276
|
+
});
|
|
250
277
|
});
|
|
251
278
|
|
|
252
279
|
describe('TrackerPage — running the extraction', () => {
|
|
@@ -113,6 +113,18 @@ export function TrackerPage({
|
|
|
113
113
|
const [error, setError] = React.useState('');
|
|
114
114
|
const [run, setRun] = React.useState<ExtractionResult | null>(null);
|
|
115
115
|
const [running, setRunning] = React.useState(false);
|
|
116
|
+
/**
|
|
117
|
+
* Does this workspace hold ANY commitments at all?
|
|
118
|
+
*
|
|
119
|
+
* Now that the server really filters, an empty page means one of two very
|
|
120
|
+
* different things and the screen owes the difference: "nothing has been
|
|
121
|
+
* pulled in yet" is advice, and saying it to somebody with ninety-seven open
|
|
122
|
+
* items and nothing overdue is simply false. Browser-verified on the deployed
|
|
123
|
+
* tenant — the Overdue tab said "Nothing has been pulled out of your meetings
|
|
124
|
+
* yet" over a corpus of ninety-seven. Resolved with ONE unfiltered
|
|
125
|
+
* count-only read, and only when a view comes back empty.
|
|
126
|
+
*/
|
|
127
|
+
const [workspaceHasAny, setWorkspaceHasAny] = React.useState<boolean | null>(null);
|
|
116
128
|
|
|
117
129
|
const query = React.useMemo<CommitmentQuery>(
|
|
118
130
|
() => ({
|
|
@@ -136,6 +148,15 @@ export function TrackerPage({
|
|
|
136
148
|
const page = await apiRef.current.listCommitments(query);
|
|
137
149
|
setRows(page.results);
|
|
138
150
|
setApplied(page.appliedFilters ?? []);
|
|
151
|
+
if (page.results.length > 0) {
|
|
152
|
+
setWorkspaceHasAny(true);
|
|
153
|
+
} else {
|
|
154
|
+
const any = await apiRef.current
|
|
155
|
+
.listCommitments({ pageSize: 1 })
|
|
156
|
+
.then((probe) => probe.count > 0)
|
|
157
|
+
.catch(() => null);
|
|
158
|
+
setWorkspaceHasAny(any);
|
|
159
|
+
}
|
|
139
160
|
} catch (err) {
|
|
140
161
|
setRows([]);
|
|
141
162
|
setError(failureMessage(err));
|
|
@@ -249,9 +270,9 @@ export function TrackerPage({
|
|
|
249
270
|
|
|
250
271
|
{!loading && visible.length === 0 ? (
|
|
251
272
|
<p className={cn.muted}>
|
|
252
|
-
{
|
|
273
|
+
{workspaceHasAny === false
|
|
253
274
|
? 'Nothing has been pulled out of your meetings yet. Connect a source, then run “Pull from meetings”.'
|
|
254
|
-
: 'Nothing matches this view.
|
|
275
|
+
: 'Nothing matches this view. Your other tabs may still have items.'}
|
|
255
276
|
</p>
|
|
256
277
|
) : null}
|
|
257
278
|
|
package/src/corpus/client.ts
CHANGED
|
@@ -125,7 +125,22 @@ interface DrfPage<T> {
|
|
|
125
125
|
*/
|
|
126
126
|
interface ServerFilterReport {
|
|
127
127
|
applied_filters?: string[];
|
|
128
|
+
/**
|
|
129
|
+
* BOTH SPELLINGS, and this is not defensive padding — it is the difference
|
|
130
|
+
* between the banner telling the truth and telling a lie.
|
|
131
|
+
*
|
|
132
|
+
* `@startsimpli/api`'s fetch wrapper converts every response key from
|
|
133
|
+
* snake_case to camelCase by default (`transformKeys`), so the tenant's
|
|
134
|
+
* `applied_filters` reaches this adapter as `appliedFilters` in the browser
|
|
135
|
+
* and as `applied_filters` from a plain server-side `fetch`. Reading only one
|
|
136
|
+
* made the deployed Tracker report "this workspace's data API did not filter
|
|
137
|
+
* by status" while the server was, verifiably, filtering by status. Same
|
|
138
|
+
* lesson as `resolveCitations` accepting `document_id`: the tolerance belongs
|
|
139
|
+
* at the seam.
|
|
140
|
+
*/
|
|
141
|
+
appliedFilters?: string[];
|
|
128
142
|
ignored_filters?: string[];
|
|
143
|
+
ignoredFilters?: string[];
|
|
129
144
|
search_backend?: string;
|
|
130
145
|
}
|
|
131
146
|
|
|
@@ -170,6 +185,22 @@ const FILTER_PARAMS: Array<[keyof CommitmentQuery, string[]]> = [
|
|
|
170
185
|
['search', ['search']],
|
|
171
186
|
];
|
|
172
187
|
|
|
188
|
+
/** Which server parameter carries which `DocumentQuery` key. */
|
|
189
|
+
const DOCUMENT_FILTER_PARAMS: Array<[keyof DocumentQuery, string[]]> = [
|
|
190
|
+
['search', ['search']],
|
|
191
|
+
['source', ['source', 'attr.source']],
|
|
192
|
+
['partyId', ['party', 'related_to']],
|
|
193
|
+
['occurredAfter', ['occurred_after', 'attr.occurred_at__gte']],
|
|
194
|
+
['occurredBefore', ['occurred_before', 'attr.occurred_at__lte']],
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
function reportedDocumentFilters(query: DocumentQuery, applied: string[]): string[] {
|
|
198
|
+
const server = new Set(applied);
|
|
199
|
+
return DOCUMENT_FILTER_PARAMS.filter(
|
|
200
|
+
([key, names]) => Boolean(query[key]) && names.some((name) => server.has(name)),
|
|
201
|
+
).map(([key]) => key);
|
|
202
|
+
}
|
|
203
|
+
|
|
173
204
|
/** Translate the server's report back into the composer's vocabulary. */
|
|
174
205
|
function reportedFilters(query: CommitmentQuery, applied: string[]): string[] {
|
|
175
206
|
const server = new Set(applied);
|
|
@@ -227,15 +258,20 @@ export function createCorpusApi(options: CreateCorpusApiOptions): CorpusApi & Co
|
|
|
227
258
|
query: DocumentQuery,
|
|
228
259
|
map: (entity: TenantEntity) => T,
|
|
229
260
|
): Promise<CorpusPage<T>> {
|
|
230
|
-
const response = await http.get<DrfPage<TenantEntity
|
|
261
|
+
const response = await http.get<DrfPage<TenantEntity> & ServerFilterReport>(entities, {
|
|
231
262
|
params: params(query, type),
|
|
232
263
|
});
|
|
264
|
+
// The SERVER's own report wins here too — the Library was reading a
|
|
265
|
+
// hardcoded list while the tenant had learned to filter.
|
|
266
|
+
const reported = response.applied_filters ?? response.appliedFilters;
|
|
233
267
|
return {
|
|
234
268
|
count: response.count ?? response.results?.length ?? 0,
|
|
235
269
|
next: response.next ?? null,
|
|
236
270
|
previous: response.previous ?? null,
|
|
237
271
|
results: (response.results ?? []).map(map),
|
|
238
|
-
appliedFilters:
|
|
272
|
+
appliedFilters: reported
|
|
273
|
+
? reportedDocumentFilters(query, reported)
|
|
274
|
+
: appliedFilters(query, supportedFilters as string[]),
|
|
239
275
|
};
|
|
240
276
|
}
|
|
241
277
|
|
|
@@ -344,6 +380,7 @@ export function createCorpusApi(options: CreateCorpusApiOptions): CorpusApi & Co
|
|
|
344
380
|
async listCommitments(query: CommitmentQuery = {}): Promise<CorpusPage<Commitment>> {
|
|
345
381
|
const params = commitmentParams(query, types.commitment);
|
|
346
382
|
const response = await http.get<DrfPage<TenantEntity> & ServerFilterReport>(entities, { params });
|
|
383
|
+
const reported = response.applied_filters ?? response.appliedFilters;
|
|
347
384
|
return {
|
|
348
385
|
count: response.count ?? response.results?.length ?? 0,
|
|
349
386
|
next: response.next ?? null,
|
|
@@ -355,8 +392,8 @@ export function createCorpusApi(options: CreateCorpusApiOptions): CorpusApi & Co
|
|
|
355
392
|
// do — and cannot claim a gap that has since been closed. The static
|
|
356
393
|
// `supportedFilters` list is only the fallback for a deployment old
|
|
357
394
|
// enough not to report.
|
|
358
|
-
appliedFilters:
|
|
359
|
-
? reportedFilters(query,
|
|
395
|
+
appliedFilters: reported
|
|
396
|
+
? reportedFilters(query, reported)
|
|
360
397
|
: appliedFilters(query as DocumentQuery, supportedFilters as string[]),
|
|
361
398
|
};
|
|
362
399
|
},
|