• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 10265336706

06 Aug 2024 11:06AM UTC coverage: 17.548% (-0.2%) from 17.734%
10265336706

Pull #793

github

web-flow
Merge e359041d4 into 1df1bc808
Pull Request #793: feat: record history

1387 of 2823 branches covered (49.13%)

6 of 1033 new or added lines in 43 files covered. (0.58%)

34 existing lines in 5 files now uncovered.

14088 of 80281 relevant lines covered (17.55%)

1.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/apps/nextjs-app/src/features/app/blocks/table/table-header/Collaborators.tsx
1
import { ColorUtils, contractColorForTheme, getCollaboratorsChannel } from '@teable/core';
×
2
import { useTheme } from '@teable/next-themes';
×
NEW
3
import type { ICollaboratorUser } from '@teable/sdk';
×
NEW
4
import { useSession, CollaboratorWithHoverCard } from '@teable/sdk';
×
5
import { useConnection } from '@teable/sdk/hooks';
×
6
import {
×
7
  cn,
×
8
  HoverCard,
×
9
  HoverCardContent,
×
10
  HoverCardTrigger,
×
11
  Popover,
×
12
  PopoverContent,
×
13
  PopoverTrigger,
×
14
} from '@teable/ui-lib/shadcn';
×
15
import { chunk, isEmpty } from 'lodash';
×
16
import { useRouter } from 'next/router';
×
17
import React, { useEffect, useMemo, useState } from 'react';
×
18
import type { Presence } from 'sharedb/lib/client';
×
19

×
20
interface CollaboratorsProps {
×
21
  className?: string;
×
22
  maxAvatarLen?: number;
×
23
}
×
24

×
25
export const Collaborators: React.FC<CollaboratorsProps> = ({ className, maxAvatarLen = 3 }) => {
×
26
  const router = useRouter();
×
27
  const { connection } = useConnection();
×
28
  const { tableId } = router.query;
×
29
  const { user: sessionUser } = useSession();
×
30
  const { resolvedTheme } = useTheme();
×
31
  const [presence, setPresence] = useState<Presence>();
×
32
  const user = useMemo(
×
33
    () => ({
×
34
      id: sessionUser.id,
×
35
      avatar: sessionUser.avatar,
×
36
      name: sessionUser.name,
×
37
      email: sessionUser.email,
×
38
    }),
×
39
    [sessionUser]
×
40
  );
×
41
  const [users, setUsers] = useState<ICollaboratorUser[]>([{ ...user }]);
×
42
  const [boardUsers, hiddenUser] = chunk(users, maxAvatarLen);
×
43

×
44
  useEffect(() => {
×
45
    if (!connection || !tableId || !user) {
×
46
      return;
×
47
    }
×
48
    const channel = getCollaboratorsChannel(tableId as string);
×
49
    setPresence(connection.getPresence(channel));
×
50
    setUsers([{ ...user }]);
×
51
  }, [connection, tableId, user]);
×
52

×
53
  useEffect(() => {
×
54
    if (!presence) {
×
55
      return;
×
56
    }
×
57

×
58
    const channelTableId = presence.channel.split('_').pop();
×
59

×
60
    if (presence.subscribed && tableId !== channelTableId) {
×
61
      return;
×
62
    }
×
63

×
64
    presence.subscribe();
×
65

×
66
    const presenceKey = `${tableId}_${user.id}`;
×
67
    const localPresence = presence.create(presenceKey);
×
68
    localPresence.submit(user, (error) => {
×
69
      error && console.error('submit error:', error);
×
70
    });
×
71

×
72
    const receiveHandler = () => {
×
73
      let newUser;
×
74
      const { remotePresences } = presence;
×
75
      if (isEmpty(remotePresences)) {
×
76
        newUser = [{ ...user }];
×
77
      } else {
×
78
        const remoteUsers = Object.values(remotePresences);
×
79
        newUser = [{ ...user }, ...remoteUsers];
×
80
      }
×
81
      setUsers(newUser);
×
82
    };
×
83

×
84
    presence.on('receive', receiveHandler);
×
85

×
86
    return () => {
×
87
      presence.unsubscribe();
×
88
      presence?.removeListener('receive', receiveHandler);
×
89
    };
×
90
  }, [connection, presence, tableId, user]);
×
91

×
92
  return (
×
93
    <div className={cn('gap-1 items-center flex', className)}>
×
94
      {boardUsers?.map(({ id, name, avatar, email }, index) => {
×
NEW
95
        const borderColor = ColorUtils.getRandomHexFromStr(`${tableId}_${id}`);
×
NEW
96

×
97
        return (
×
98
          <HoverCard key={`${id}_${index}`}>
×
99
            <HoverCardTrigger asChild>
×
100
              <div className="relative overflow-hidden">
×
NEW
101
                <CollaboratorWithHoverCard
×
NEW
102
                  id={id}
×
NEW
103
                  name={name}
×
NEW
104
                  avatar={avatar}
×
NEW
105
                  email={email}
×
NEW
106
                  borderColor={contractColorForTheme(borderColor, resolvedTheme)}
×
NEW
107
                />
×
108
              </div>
×
109
            </HoverCardTrigger>
×
110
            <HoverCardContent className="flex w-max max-w-[160px] flex-col justify-center truncate p-2 text-sm">
×
111
              <div className="truncate">
×
112
                <span title={name}>{name}</span>
×
113
                <span className="pl-1">{id === user.id ? '(You)' : null}</span>
×
114
              </div>
×
115
              <div className="truncate">
×
116
                <span title={email}>{email}</span>
×
117
              </div>
×
118
            </HoverCardContent>
×
119
          </HoverCard>
×
120
        );
×
121
      })}
×
122
      {hiddenUser ? (
×
123
        <Popover>
×
124
          <PopoverTrigger asChild>
×
125
            <div className="relative size-6 shrink-0 grow-0 cursor-pointer select-none overflow-hidden rounded-full border-slate-200">
×
126
              <p className="flex size-full items-center justify-center rounded-full border-2 text-center text-xs">
×
127
                +{hiddenUser.length}
×
128
              </p>
×
129
            </div>
×
130
          </PopoverTrigger>
×
131
          <PopoverContent className="max-h-64 w-36 overflow-y-auto">
×
NEW
132
            {hiddenUser.map(({ id, name, avatar, email }) => {
×
NEW
133
              const borderColor = ColorUtils.getRandomHexFromStr(`${tableId}_${id}`);
×
NEW
134
              return (
×
NEW
135
                <div key={id} className="flex items-center truncate p-1">
×
NEW
136
                  <CollaboratorWithHoverCard
×
NEW
137
                    id={id}
×
NEW
138
                    name={name}
×
NEW
139
                    avatar={avatar}
×
NEW
140
                    email={email}
×
NEW
141
                    borderColor={contractColorForTheme(borderColor, resolvedTheme)}
×
NEW
142
                  />
×
NEW
143
                  <div className="flex-1 truncate pl-1">{name}</div>
×
NEW
144
                </div>
×
NEW
145
              );
×
NEW
146
            })}
×
147
          </PopoverContent>
×
148
        </Popover>
×
149
      ) : null}
×
150
    </div>
×
151
  );
×
152
};
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc