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
|
-
|
|
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
|
|
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
|
-
//
|
|
1498
|
-
//
|
|
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
|
-
|
|
1524
|
-
|
|
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
|
-
|
|
1529
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|