@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.9",
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
- {rows.length === 0
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. Everything here is either closed or filtered out.'}
275
+ : 'Nothing matches this view. Your other tabs may still have items.'}
255
276
  </p>
256
277
  ) : null}
257
278
 
@@ -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>>(entities, {
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: appliedFilters(query, supportedFilters),
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: response.applied_filters
359
- ? reportedFilters(query, response.applied_filters)
395
+ appliedFilters: reported
396
+ ? reportedFilters(query, reported)
360
397
  : appliedFilters(query as DocumentQuery, supportedFilters as string[]),
361
398
  };
362
399
  },