twenty-bells 1.0.0 → 1.0.2

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.
@@ -751,9 +751,6 @@ const ActivityFeed = () => {
751
751
  const [myCompanyIds, setMyCompanyIds] = useState<string[]>([]);
752
752
  const [memberNames, setMemberNames] = useState<Record<string, string>>({});
753
753
  const [memberAvatars, setMemberAvatars] = useState<Record<string, string>>({});
754
- const [enforcement, setEnforcement] = useState<
755
- 'server' | 'client' | 'unknown'
756
- >('unknown');
757
754
  const [view, setView] = useState<'feed' | 'tasks' | 'buzz'>('feed');
758
755
  const [tasks, setTasks] = useState<TimelineRecord[]>([]);
759
756
  // Every task assigned to the reader that is not done — the whole workspace,
@@ -1005,8 +1002,20 @@ const ActivityFeed = () => {
1005
1002
  ),
1006
1003
  );
1007
1004
 
1005
+ // Asked for by id rather than looked up in the page above: the member
1006
+ // list is capped at two hundred, and in a workspace larger than that the
1007
+ // viewer may simply not be on it. Failing to place them used to mean
1008
+ // failing to filter.
1009
+ const self = await client.get<{
1010
+ data?: { workspaceMembers?: { id: string }[] };
1011
+ }>('/rest/workspaceMembers', {
1012
+ query: { filter: `userId[eq]:${userId}`, limit: 1 },
1013
+ });
1014
+
1008
1015
  const id =
1009
- allMembers.find((member) => member.userId === userId)?.id ?? null;
1016
+ self.data?.workspaceMembers?.[0]?.id ??
1017
+ allMembers.find((member) => member.userId === userId)?.id ??
1018
+ null;
1010
1019
 
1011
1020
  setMemberId(id);
1012
1021
 
@@ -1021,26 +1030,6 @@ const ActivityFeed = () => {
1021
1030
  });
1022
1031
 
1023
1032
  setMyCompanyIds((companies.data?.companies ?? []).map((c) => c.id));
1024
-
1025
-
1026
- // Is row-level security actually enforced? Row-level permissions are an
1027
- // Organization-plan feature, so the same build lands on instances where
1028
- // the predicates in our role are live and on ones where they are inert.
1029
- // Rather than guess the plan, ask the server for records it should have
1030
- // withheld: if a deal owned by somebody else comes back, enforcement is
1031
- // off and the panel has to filter by itself.
1032
- const probe = await client.get<{
1033
- data?: { opportunities?: { ownerId?: string | null }[] };
1034
- }>('/rest/opportunities', { query: { limit: 100 } });
1035
-
1036
- const owned = (probe.data?.opportunities ?? []).filter(
1037
- (row) => typeof row.ownerId === 'string' && row.ownerId !== '',
1038
- );
1039
- const foreign = owned.filter((row) => row.ownerId !== id);
1040
-
1041
- setEnforcement(
1042
- foreign.length > 0 ? 'client' : owned.length > 0 ? 'server' : 'unknown',
1043
- );
1044
1033
  } catch {
1045
1034
  // Without a resolved member the panel falls back to showing everything.
1046
1035
  }
@@ -1409,8 +1398,11 @@ const ActivityFeed = () => {
1409
1398
  // stop the data reaching the browser. The app role is what actually governs
1410
1399
  // access, and it is workspace-wide.
1411
1400
  const isMine = (item: TimelineRecord) => {
1401
+ // Nobody's feed is everybody's feed. If the viewer cannot be placed in the
1402
+ // workspace there is no "mine" to compute, and showing the lot instead is
1403
+ // the opposite of what this panel is for.
1412
1404
  if (memberId === null) {
1413
- return true;
1405
+ return false;
1414
1406
  }
1415
1407
 
1416
1408
  // Anything you did is yours. Events made through the API carry no
@@ -1494,23 +1486,20 @@ const ActivityFeed = () => {
1494
1486
  [items, olderItems, documentItems],
1495
1487
  );
1496
1488
 
1497
- // On an enforcing instance the server already scoped the response, so
1498
- // filtering again would only hide records the viewer is entitled to.
1489
+ // The panel is personal, full stop. A toggle to "everything" made the counter
1490
+ // meaningless a number under a feed that showed a fraction of it — and
1491
+ // nobody reads a workspace-wide firehose anyway. The variant with the switch
1492
+ // is kept on the backup/scope-toggle branch.
1493
+ //
1494
+ // This filter used to be skipped where the server appeared to scope the data
1495
+ // itself. It does not any more: server-side scoping can only ever narrow the
1496
+ // response further, so running the personal filter on top of it is always
1497
+ // safe, while trusting it was not.
1499
1498
  const visibleItems = useMemo(
1500
- () =>
1501
- // The panel is personal, full stop. A toggle to "everything" made the
1502
- // counter meaningless — a number under a feed that showed a fraction of
1503
- // it — and nobody reads a workspace-wide firehose anyway. The variant
1504
- // with the switch is kept on the backup/scope-toggle branch.
1505
- enforcement === 'server' ? allItems : allItems.filter(isMine),
1499
+ () => allItems.filter(isMine),
1506
1500
  // `isMine` is rebuilt every render; what it actually reads is listed here.
1507
1501
  // eslint-disable-next-line react-hooks/exhaustive-deps
1508
- [
1509
- allItems,
1510
- enforcement,
1511
- memberId,
1512
- myCompanyIds,
1513
- ],
1502
+ [allItems, memberId, myCompanyIds],
1514
1503
  );
1515
1504
 
1516
1505
  // Tasks view: what is hanging, not what changed. Done tasks are dropped —
@@ -1520,13 +1509,12 @@ const ActivityFeed = () => {
1520
1509
  return false;
1521
1510
  }
1522
1511
 
1523
- if (enforcement === 'server' || memberId === null) {
1524
- return true;
1525
- }
1526
-
1512
+ // A task belongs to the two people it names. One with neither — no
1513
+ // assignee, set by somebody else — belongs to nobody here.
1527
1514
  return (
1528
- task.assigneeId === memberId ||
1529
- readMemberId(task.createdBy) === memberId
1515
+ memberId !== null &&
1516
+ (task.assigneeId === memberId ||
1517
+ readMemberId(task.createdBy) === memberId)
1530
1518
  );
1531
1519
  });
1532
1520
 
@@ -2475,7 +2463,12 @@ const ActivityFeed = () => {
2475
2463
  const renderTask = (task: TimelineRecord) => {
2476
2464
  const due = describeDue(task.dueAt);
2477
2465
  const isOverdue = due.overdueDays > 0;
2478
- const title = typeof task.title === 'string' ? task.title : t('Untitled');
2466
+ // An empty string is a string: without this a nameless task drew a card
2467
+ // with a status, a date and no line saying what it was.
2468
+ const title =
2469
+ typeof task.title === 'string' && task.title.trim() !== ''
2470
+ ? task.title
2471
+ : t('Untitled');
2479
2472
  // The task arrives without its relations, so the assignee is resolved
2480
2473
  // through the member map the panel already holds.
2481
2474
  const assignee =
@@ -2864,7 +2857,10 @@ const ActivityFeed = () => {
2864
2857
 
2865
2858
  const renderBuzzPost = (post: (typeof buzzPosts)[number]) => {
2866
2859
  const { note, comments, originalBody, touchedAt } = post;
2867
- const title = typeof note.title === 'string' ? note.title : t('Untitled');
2860
+ const title =
2861
+ typeof note.title === 'string' && note.title.trim() !== ''
2862
+ ? note.title
2863
+ : t('Untitled');
2868
2864
  const author = readDisplayName(note.createdBy);
2869
2865
  const noteColor = getObjectColor('note');
2870
2866
 
@@ -3388,32 +3384,6 @@ const ActivityFeed = () => {
3388
3384
  />
3389
3385
  )}
3390
3386
  </div>
3391
-
3392
- <div
3393
- style={{ display: 'flex', gap: '6px', flexShrink: 0, whiteSpace: 'nowrap' }}
3394
- >
3395
- {view === 'buzz' ? (
3396
- // Buzz is the team wall: everyone's notes, always. A personal
3397
- // filter there would contradict what the section is for.
3398
- <></>
3399
- ) : enforcement === 'server' ? (
3400
- <span
3401
- title={t("Row-level permissions are on: Twenty scopes the data itself and the panel adds no filtering.")}
3402
- style={{
3403
- padding: '4px 9px',
3404
- borderRadius: '4px',
3405
- background: palette.buttonBackground,
3406
- color: palette.textMid,
3407
- fontSize: '0.85rem',
3408
- fontWeight: 400,
3409
- whiteSpace: 'nowrap',
3410
- flexShrink: 0,
3411
- }}
3412
- >
3413
- {t('Access: server')}
3414
- </span>
3415
- ) : null}
3416
- </div>
3417
3387
  </div>
3418
3388
 
3419
3389
  <div