@startsimpli/integrations 0.2.7 → 0.2.9
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.9",
|
|
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",
|
|
@@ -299,3 +299,77 @@ describe('the tenant’s filters are still not honoured, and are still not faked
|
|
|
299
299
|
expect(page.count).toBe(FIXTURE_TENANT_DOCUMENTS.length);
|
|
300
300
|
});
|
|
301
301
|
});
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// the tracker's read, against the tenant's real filter vocabulary
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
describe('listCommitments speaks the tenant data API’s own filter language', () => {
|
|
308
|
+
function client(applied: string[] | undefined) {
|
|
309
|
+
const seen: Array<{ path: string; params: Record<string, unknown> }> = [];
|
|
310
|
+
const api = createCorpusApi({
|
|
311
|
+
http: {
|
|
312
|
+
async get<T>(path: string, options?: { params?: Record<string, unknown> }): Promise<T> {
|
|
313
|
+
seen.push({ path, params: options?.params ?? {} });
|
|
314
|
+
return {
|
|
315
|
+
count: 0, next: null, previous: null, results: [],
|
|
316
|
+
...(applied ? { applied_filters: applied, ignored_filters: [] } : {}),
|
|
317
|
+
} as T;
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
scope: { orgId: 'tandem' },
|
|
321
|
+
supportedFilters: [],
|
|
322
|
+
});
|
|
323
|
+
return { api, seen };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
it('sends status, owner, due window and order as attr.* parameters', async () => {
|
|
327
|
+
const { api, seen } = client(['attr.status__in']);
|
|
328
|
+
await api.listCommitments({
|
|
329
|
+
status: ['open', 'blocked'],
|
|
330
|
+
owner: 'Kate',
|
|
331
|
+
dueBefore: '2026-08-10T00:00:00.000Z',
|
|
332
|
+
changedAfter: '2026-08-03T00:00:00.000Z',
|
|
333
|
+
pageSize: 200,
|
|
334
|
+
});
|
|
335
|
+
expect(seen[0].params).toMatchObject({
|
|
336
|
+
type: 'commitment',
|
|
337
|
+
'attr.status__in': 'open,blocked',
|
|
338
|
+
'attr.owner_name__icontains': 'Kate',
|
|
339
|
+
'attr.due_at__lte': '2026-08-10T00:00:00.000Z',
|
|
340
|
+
'attr.last_seen_at__gte': '2026-08-03T00:00:00.000Z',
|
|
341
|
+
order: 'attr.due_at',
|
|
342
|
+
page_size: 200,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('caps a text filter at the length the server documents', async () => {
|
|
347
|
+
const { api, seen } = client([]);
|
|
348
|
+
await api.listCommitments({ owner: 'x'.repeat(900), search: 'y'.repeat(900) });
|
|
349
|
+
expect(String(seen[0].params['attr.owner_name__icontains'])).toHaveLength(500);
|
|
350
|
+
expect(String(seen[0].params.search)).toHaveLength(500);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('caps page size at the server’s maximum rather than asking for more', async () => {
|
|
354
|
+
const { api, seen } = client([]);
|
|
355
|
+
await api.listCommitments({ pageSize: 5000 });
|
|
356
|
+
expect(seen[0].params.page_size).toBe(200);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('believes the SERVER about what it filtered, not a hardcoded list', async () => {
|
|
360
|
+
// The static `supportedFilters` says nothing is supported. The server says
|
|
361
|
+
// it applied status. The server wins — otherwise the screen keeps apologising
|
|
362
|
+
// for a gap that was closed while it was not looking.
|
|
363
|
+
const { api } = client(['attr.status__in', 'attr.due_at__lte']);
|
|
364
|
+
const page = await api.listCommitments({ status: ['open', 'blocked'], dueBefore: 'x', owner: 'Kate' });
|
|
365
|
+
expect(page.appliedFilters).toEqual(['status', 'dueBefore']);
|
|
366
|
+
// And owner is NOT claimed, because the server did not report it.
|
|
367
|
+
expect(page.appliedFilters).not.toContain('owner');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('falls back to the declared list on a server that does not report', async () => {
|
|
371
|
+
const { api } = client(undefined);
|
|
372
|
+
const page = await api.listCommitments({ status: 'open' });
|
|
373
|
+
expect(page.appliedFilters).toEqual([]);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* as though it had one, a filter that did nothing, a row with no traceable
|
|
8
8
|
* source, a run that closed something and did not say why.
|
|
9
9
|
*/
|
|
10
|
+
import * as React from 'react';
|
|
10
11
|
import { render, screen, within } from '@testing-library/react';
|
|
11
12
|
import userEvent from '@testing-library/user-event';
|
|
12
13
|
import { describe, expect, it, vi } from 'vitest';
|
|
@@ -92,6 +93,61 @@ function makeApi(options: {
|
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
describe('TrackerPage — it must not loop', () => {
|
|
97
|
+
/**
|
|
98
|
+
* SHIPPED TO PRODUCTION AND CAUGHT BY A HUMAN, not by this suite: 917
|
|
99
|
+
* `entities` requests in ten minutes on the live tenant, the screen visibly
|
|
100
|
+
* flashing. The cause was three lines that each looked reasonable:
|
|
101
|
+
*
|
|
102
|
+
* now = Date.now() a defaulted prop — a NEW value per render
|
|
103
|
+
* useMemo(..., [tab, owner, now]) so a new query object per render
|
|
104
|
+
* useCallback(..., [api, query]) so a new loader per render
|
|
105
|
+
* useEffect(..., [load]) so a fetch per render -> setState -> render
|
|
106
|
+
*
|
|
107
|
+
* A defaulted `Date.now()` can never be a stable dependency. These tests count
|
|
108
|
+
* requests, because that is the only thing that catches this: at 1.5 requests
|
|
109
|
+
* a second the screenshot looks perfect.
|
|
110
|
+
*/
|
|
111
|
+
it('fetches ONCE on mount and then stops', async () => {
|
|
112
|
+
let calls = 0;
|
|
113
|
+
const api = makeApi({ onQuery: () => { calls += 1; } });
|
|
114
|
+
render(<TrackerPage api={api} scope={SCOPE} />);
|
|
115
|
+
await screen.findByText('Chase the inventor for comments on 2148');
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
117
|
+
expect(calls).toBe(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does not loop even when the app hands it a NEW api object every render', async () => {
|
|
121
|
+
// The composer takes an injected capability. A caller that builds it inline
|
|
122
|
+
// is a mistake, but it must not be a runaway one — the identity of an api is
|
|
123
|
+
// not a reason to re-read the same query.
|
|
124
|
+
let calls = 0;
|
|
125
|
+
function Harness() {
|
|
126
|
+
const [, force] = React.useState(0);
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
const timer = setInterval(() => force((value) => value + 1), 20);
|
|
129
|
+
return () => clearInterval(timer);
|
|
130
|
+
}, []);
|
|
131
|
+
// Deliberately unmemoized.
|
|
132
|
+
return <TrackerPage api={makeApi({ onQuery: () => { calls += 1; } })} scope={SCOPE} />;
|
|
133
|
+
}
|
|
134
|
+
render(<Harness />);
|
|
135
|
+
await screen.findByText('Chase the inventor for comments on 2148');
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
137
|
+
expect(calls).toBe(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('DOES re-read when the user actually changes something', async () => {
|
|
141
|
+
let calls = 0;
|
|
142
|
+
render(<TrackerPage api={makeApi({ onQuery: () => { calls += 1; } })} scope={SCOPE} now={NOW} />);
|
|
143
|
+
await screen.findByText('Chase the inventor for comments on 2148');
|
|
144
|
+
expect(calls).toBe(1);
|
|
145
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Overdue' }));
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
147
|
+
expect(calls).toBe(2);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
95
151
|
describe('TrackerPage — what is outstanding', () => {
|
|
96
152
|
it('shows open items and hides closed ones', async () => {
|
|
97
153
|
render(<TrackerPage api={makeApi()} scope={SCOPE} now={NOW} />);
|
|
@@ -116,7 +172,7 @@ describe('TrackerPage — what is outstanding', () => {
|
|
|
116
172
|
// The Library's `appliedFilters` contract, owed here about status — a
|
|
117
173
|
// status filter that silently does nothing breaks the screen's only claim.
|
|
118
174
|
expect(
|
|
119
|
-
await screen.findByText(/
|
|
175
|
+
await screen.findByText(/did not filter by .*status/i),
|
|
120
176
|
).toBeInTheDocument();
|
|
121
177
|
});
|
|
122
178
|
|
|
@@ -129,7 +185,7 @@ describe('TrackerPage — what is outstanding', () => {
|
|
|
129
185
|
/>,
|
|
130
186
|
);
|
|
131
187
|
await screen.findByText('Chase the inventor for comments on 2148');
|
|
132
|
-
expect(screen.queryByText(/
|
|
188
|
+
expect(screen.queryByText(/did not filter by/i)).not.toBeInTheDocument();
|
|
133
189
|
});
|
|
134
190
|
|
|
135
191
|
it('puts an overdue item above one with no due date', async () => {
|
|
@@ -41,7 +41,15 @@ export interface TrackerPageProps {
|
|
|
41
41
|
scope: CorpusScope;
|
|
42
42
|
/** Click-through from a mention to the record, normally the Library route. */
|
|
43
43
|
onOpenDocument?: (documentId: string) => void;
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Injectable clock, so "overdue" and "this week" are testable.
|
|
46
|
+
*
|
|
47
|
+
* NOT DEFAULTED IN THE SIGNATURE. `now = Date.now()` is a new value on every
|
|
48
|
+
* render, which makes every memo below it new, which makes the load effect
|
|
49
|
+
* fire on every render, which sets state, which renders. That shipped, and it
|
|
50
|
+
* cost 917 requests in ten minutes on a live tenant. When the caller does not
|
|
51
|
+
* supply a clock, one is frozen ONCE for the life of the component.
|
|
52
|
+
*/
|
|
45
53
|
now?: number;
|
|
46
54
|
classNames?: Partial<Record<TrackerSlot, string>>;
|
|
47
55
|
}
|
|
@@ -79,11 +87,24 @@ export function TrackerPage({
|
|
|
79
87
|
api,
|
|
80
88
|
scope,
|
|
81
89
|
onOpenDocument,
|
|
82
|
-
now
|
|
90
|
+
now,
|
|
83
91
|
classNames = {},
|
|
84
92
|
}: TrackerPageProps) {
|
|
85
93
|
const cn = { ...DEFAULTS, ...classNames };
|
|
86
94
|
|
|
95
|
+
// Frozen once. See the note on the prop.
|
|
96
|
+
const [mountedAt] = React.useState(() => Date.now());
|
|
97
|
+
const clock = now ?? mountedAt;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The api is an injected CAPABILITY, not data, so its identity is not a reason
|
|
101
|
+
* to re-read the same query — and an app that builds it inline must not turn
|
|
102
|
+
* that mistake into a runaway. Held in a ref and read at call time; a genuine
|
|
103
|
+
* change of workspace re-reads through `scope.orgId` below.
|
|
104
|
+
*/
|
|
105
|
+
const apiRef = React.useRef(api);
|
|
106
|
+
apiRef.current = api;
|
|
107
|
+
|
|
87
108
|
const [tab, setTab] = React.useState<'open' | 'overdue' | 'recent' | 'done'>('open');
|
|
88
109
|
const [owner, setOwner] = React.useState('');
|
|
89
110
|
const [rows, setRows] = React.useState<Commitment[]>([]);
|
|
@@ -101,18 +122,18 @@ export function TrackerPage({
|
|
|
101
122
|
// and the banner alone would be doing all the work.
|
|
102
123
|
status: tab === 'done' ? 'done' : (['open', 'blocked'] as CommitmentStatus[]),
|
|
103
124
|
...(owner.trim() ? { owner: owner.trim() } : {}),
|
|
104
|
-
...(tab === 'recent' ? { changedAfter: new Date(
|
|
105
|
-
...(tab === 'overdue' ? { dueBefore: new Date(
|
|
125
|
+
...(tab === 'recent' ? { changedAfter: new Date(clock - WEEK_MS).toISOString() } : {}),
|
|
126
|
+
...(tab === 'overdue' ? { dueBefore: new Date(clock).toISOString() } : {}),
|
|
106
127
|
pageSize: 200,
|
|
107
128
|
}),
|
|
108
|
-
[tab, owner,
|
|
129
|
+
[tab, owner, clock],
|
|
109
130
|
);
|
|
110
131
|
|
|
111
132
|
const load = React.useCallback(async () => {
|
|
112
133
|
setLoading(true);
|
|
113
134
|
setError('');
|
|
114
135
|
try {
|
|
115
|
-
const page = await
|
|
136
|
+
const page = await apiRef.current.listCommitments(query);
|
|
116
137
|
setRows(page.results);
|
|
117
138
|
setApplied(page.appliedFilters ?? []);
|
|
118
139
|
} catch (err) {
|
|
@@ -121,35 +142,36 @@ export function TrackerPage({
|
|
|
121
142
|
} finally {
|
|
122
143
|
setLoading(false);
|
|
123
144
|
}
|
|
124
|
-
}, [
|
|
145
|
+
}, [query]);
|
|
125
146
|
|
|
126
|
-
|
|
147
|
+
// Real input only: the query the user asked for, and the workspace they are in.
|
|
148
|
+
React.useEffect(() => { void load(); }, [load, scope.orgId]);
|
|
127
149
|
|
|
128
150
|
/**
|
|
129
151
|
* The tab, applied in the browser as well as sent to the server.
|
|
130
152
|
*
|
|
131
|
-
* NOT a violation of rule 8 — the query
|
|
132
|
-
* server
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
153
|
+
* NOT a violation of rule 8 — the query goes out as `attr.*` parameters and
|
|
154
|
+
* the server pages and filters. This is the belt to the server's braces: a
|
|
155
|
+
* deployment whose API ignores one of them must not end up rendering closed
|
|
156
|
+
* items under "Outstanding", which is the one thing this screen exists to say.
|
|
157
|
+
* Re-applying a filter the server already applied is a no-op; not re-applying
|
|
158
|
+
* one it ignored is a lie. The banner below fires only when the SERVER itself
|
|
159
|
+
* reports it did not apply something.
|
|
138
160
|
*/
|
|
139
161
|
const visible = React.useMemo(() => {
|
|
140
162
|
const byTab = rows.filter((item) => {
|
|
141
163
|
if (tab === 'done') return item.status === 'done' || item.status === 'dropped';
|
|
142
164
|
if (item.status !== 'open' && item.status !== 'blocked') return false;
|
|
143
|
-
if (tab === 'overdue') return isOverdue(item,
|
|
144
|
-
if (tab === 'recent') return changedSince(item,
|
|
165
|
+
if (tab === 'overdue') return isOverdue(item, clock);
|
|
166
|
+
if (tab === 'recent') return changedSince(item, clock - WEEK_MS);
|
|
145
167
|
return true;
|
|
146
168
|
});
|
|
147
169
|
const needle = owner.trim().toLowerCase();
|
|
148
170
|
const byOwner = needle
|
|
149
171
|
? byTab.filter((item) => (item.ownerName ?? '').toLowerCase().includes(needle))
|
|
150
172
|
: byTab;
|
|
151
|
-
return [...byOwner].sort(compareForScreen(
|
|
152
|
-
}, [rows, tab, owner,
|
|
173
|
+
return [...byOwner].sort(compareForScreen(clock));
|
|
174
|
+
}, [rows, tab, owner, clock]);
|
|
153
175
|
|
|
154
176
|
const unfiltered = (['status', 'owner', 'dueBefore', 'changedAfter'] as const).filter(
|
|
155
177
|
(key) => Boolean(query[key]) && !applied.includes(key),
|
|
@@ -191,7 +213,7 @@ export function TrackerPage({
|
|
|
191
213
|
setRunning(true);
|
|
192
214
|
setError('');
|
|
193
215
|
try {
|
|
194
|
-
setRun(await
|
|
216
|
+
setRun(await apiRef.current.extract!({ scope }));
|
|
195
217
|
await load();
|
|
196
218
|
} catch (err) {
|
|
197
219
|
setError(failureMessage(err));
|
|
@@ -210,7 +232,7 @@ export function TrackerPage({
|
|
|
210
232
|
// status — and it matters more here, because "outstanding" is the
|
|
211
233
|
// claim the whole screen makes.
|
|
212
234
|
<p className={cn.warning} role="status">
|
|
213
|
-
This workspace’s data API
|
|
235
|
+
This workspace’s data API did not filter by {unfiltered.join(', ')}, so this list was
|
|
214
236
|
narrowed in the browser over the records it could fetch. Counts are over that set, not
|
|
215
237
|
over the whole corpus.
|
|
216
238
|
</p>
|
|
@@ -237,7 +259,7 @@ export function TrackerPage({
|
|
|
237
259
|
<CommitmentRow
|
|
238
260
|
key={item.externalId}
|
|
239
261
|
item={item}
|
|
240
|
-
now={
|
|
262
|
+
now={clock}
|
|
241
263
|
classNames={cn}
|
|
242
264
|
onOpenDocument={onOpenDocument}
|
|
243
265
|
/>
|
package/src/corpus/client.ts
CHANGED
|
@@ -114,6 +114,70 @@ interface DrfPage<T> {
|
|
|
114
114
|
results: T[];
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* What the tenant data API reports back about the query it was handed.
|
|
119
|
+
*
|
|
120
|
+
* `applied_filters` and `ignored_filters` are the whole reason a composer can
|
|
121
|
+
* stop guessing: a screen that narrows its own list has to know whether it is
|
|
122
|
+
* doing so because the server could not, or is duplicating work the server
|
|
123
|
+
* already did. Reported per response, so a capability that lands mid-week is
|
|
124
|
+
* picked up without a client deploy.
|
|
125
|
+
*/
|
|
126
|
+
interface ServerFilterReport {
|
|
127
|
+
applied_filters?: string[];
|
|
128
|
+
ignored_filters?: string[];
|
|
129
|
+
search_backend?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `CommitmentQuery` -> the tenant's own parameter vocabulary.
|
|
134
|
+
*
|
|
135
|
+
* Attribute filters are `attr.<declared name>` with an optional operator suffix,
|
|
136
|
+
* which is why every field the tracker filters on has to be a DECLARED
|
|
137
|
+
* AttributeDef — an undeclared attribute is invisible to the index and the
|
|
138
|
+
* server reports it under `ignored_filters` rather than silently returning
|
|
139
|
+
* everything.
|
|
140
|
+
*/
|
|
141
|
+
const TEXT_FILTER_MAX = 500;
|
|
142
|
+
|
|
143
|
+
function commitmentParams(query: CommitmentQuery, type: string): Record<string, unknown> {
|
|
144
|
+
const statuses = Array.isArray(query.status) ? query.status : query.status ? [query.status] : [];
|
|
145
|
+
const out: Record<string, unknown> = {
|
|
146
|
+
type,
|
|
147
|
+
page: query.page ?? 1,
|
|
148
|
+
...(query.pageSize ? { page_size: Math.min(query.pageSize, 200) } : {}),
|
|
149
|
+
// Oldest debt first, which is what the screen sorts by anyway — asking the
|
|
150
|
+
// server for it means page one is the right page one.
|
|
151
|
+
order: 'attr.due_at',
|
|
152
|
+
};
|
|
153
|
+
if (statuses.length === 1) out['attr.status'] = statuses[0];
|
|
154
|
+
else if (statuses.length > 1) out['attr.status__in'] = statuses.join(',');
|
|
155
|
+
if (query.subtype) out['attr.subtype'] = query.subtype;
|
|
156
|
+
if (query.owner) out['attr.owner_name__icontains'] = query.owner.slice(0, TEXT_FILTER_MAX);
|
|
157
|
+
if (query.dueBefore) out['attr.due_at__lte'] = query.dueBefore;
|
|
158
|
+
if (query.changedAfter) out['attr.last_seen_at__gte'] = query.changedAfter;
|
|
159
|
+
if (query.search) out.search = query.search.slice(0, TEXT_FILTER_MAX);
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Which server parameter carries which `CommitmentQuery` key. */
|
|
164
|
+
const FILTER_PARAMS: Array<[keyof CommitmentQuery, string[]]> = [
|
|
165
|
+
['status', ['attr.status', 'attr.status__in']],
|
|
166
|
+
['subtype', ['attr.subtype']],
|
|
167
|
+
['owner', ['attr.owner_name__icontains']],
|
|
168
|
+
['dueBefore', ['attr.due_at__lte']],
|
|
169
|
+
['changedAfter', ['attr.last_seen_at__gte']],
|
|
170
|
+
['search', ['search']],
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/** Translate the server's report back into the composer's vocabulary. */
|
|
174
|
+
function reportedFilters(query: CommitmentQuery, applied: string[]): string[] {
|
|
175
|
+
const server = new Set(applied);
|
|
176
|
+
return FILTER_PARAMS.filter(
|
|
177
|
+
([key, params]) => Boolean(query[key]) && params.some((param) => server.has(param)),
|
|
178
|
+
).map(([key]) => key);
|
|
179
|
+
}
|
|
180
|
+
|
|
117
181
|
const EMPTY_PAGE = { count: 0, next: null, previous: null, results: [] };
|
|
118
182
|
|
|
119
183
|
/** Rows written in parallel. Small enough not to look like a burst to the tenant. */
|
|
@@ -278,28 +342,22 @@ export function createCorpusApi(options: CreateCorpusApiOptions): CorpusApi & Co
|
|
|
278
342
|
* than rendering an unfiltered list under a filled-in control.
|
|
279
343
|
*/
|
|
280
344
|
async listCommitments(query: CommitmentQuery = {}): Promise<CorpusPage<Commitment>> {
|
|
281
|
-
const
|
|
282
|
-
const response = await http.get<DrfPage<TenantEntity
|
|
283
|
-
params: {
|
|
284
|
-
type: types.commitment,
|
|
285
|
-
page: query.page ?? 1,
|
|
286
|
-
...(query.pageSize ? { page_size: query.pageSize } : {}),
|
|
287
|
-
...(statuses.length ? { status: statuses.join(',') } : {}),
|
|
288
|
-
...(query.subtype ? { subtype: query.subtype } : {}),
|
|
289
|
-
...(query.owner ? { owner: query.owner } : {}),
|
|
290
|
-
...(query.dueBefore ? { due_before: query.dueBefore } : {}),
|
|
291
|
-
...(query.changedAfter ? { changed_after: query.changedAfter } : {}),
|
|
292
|
-
...(query.search ? { search: query.search } : {}),
|
|
293
|
-
},
|
|
294
|
-
});
|
|
345
|
+
const params = commitmentParams(query, types.commitment);
|
|
346
|
+
const response = await http.get<DrfPage<TenantEntity> & ServerFilterReport>(entities, { params });
|
|
295
347
|
return {
|
|
296
348
|
count: response.count ?? response.results?.length ?? 0,
|
|
297
349
|
next: response.next ?? null,
|
|
298
350
|
previous: response.previous ?? null,
|
|
299
351
|
results: (response.results ?? []).map((entity) => toCommitment(entity, scope, sourceLabels)),
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
352
|
+
// THE SERVER'S OWN ANSWER WINS. `applied_filters` is reported by the
|
|
353
|
+
// tenant data API (capabilities.entities.reports_applied_filters), so
|
|
354
|
+
// the screen no longer has to be told out-of-band what the backend can
|
|
355
|
+
// do — and cannot claim a gap that has since been closed. The static
|
|
356
|
+
// `supportedFilters` list is only the fallback for a deployment old
|
|
357
|
+
// enough not to report.
|
|
358
|
+
appliedFilters: response.applied_filters
|
|
359
|
+
? reportedFilters(query, response.applied_filters)
|
|
360
|
+
: appliedFilters(query as DocumentQuery, supportedFilters as string[]),
|
|
303
361
|
};
|
|
304
362
|
},
|
|
305
363
|
|