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

apowers313 / aiforge / 20962792399

13 Jan 2026 03:39PM UTC coverage: 84.707%. First build
20962792399

push

github

apowers313
feat: initial commit

787 of 905 branches covered (86.96%)

Branch coverage included in aggregate %.

4248 of 5039 new or added lines in 70 files covered. (84.3%)

4248 of 5039 relevant lines covered (84.3%)

13.11 hits per line

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

96.91
/src/client/components/projects/AddProjectModal.tsx
1
import { useState, useEffect, useCallback } from 'react';
1✔
2
import { Modal, Box, Group, Button, Text, Stack, UnstyledButton, Breadcrumbs, Anchor, Loader, Alert, Center } from '@mantine/core';
1✔
3
import { IconFolder, IconAlertCircle } from '@tabler/icons-react';
1✔
4
import { api, type DirectoryEntry } from '@client/services/api';
1✔
5

6
interface AddProjectModalProps {
7
  opened: boolean;
8
  onClose: () => void;
9
  onSelect: (path: string) => void;
10
}
11

12
interface BreadcrumbItem {
13
  name: string;
14
  path: string;
15
}
16

17
function pathToBreadcrumbs(path: string, homePath: string): BreadcrumbItem[] {
45✔
18
  if (!homePath || !path) {
45✔
19
    return [];
28✔
20
  }
28✔
21

22
  // Get the username from home path (last segment)
23
  const homeSegments = homePath.split('/').filter(Boolean);
17✔
24
  const username = homeSegments[homeSegments.length - 1] ?? 'home';
45!
25

26
  const breadcrumbs: BreadcrumbItem[] = [{ name: username, path: homePath }];
45✔
27

28
  // Only add breadcrumbs for paths beyond the home directory
29
  if (path.startsWith(homePath) && path !== homePath) {
45✔
30
    const relativePath = path.slice(homePath.length);
3✔
31
    const parts = relativePath.split('/').filter(Boolean);
3✔
32
    let currentPath = homePath;
3✔
33
    for (const part of parts) {
3✔
34
      currentPath += '/' + part;
3✔
35
      breadcrumbs.push({ name: part, path: currentPath });
3✔
36
    }
3✔
37
  }
3✔
38

39
  return breadcrumbs;
17✔
40
}
17✔
41

42
export function AddProjectModal({ opened, onClose, onSelect }: AddProjectModalProps): React.ReactElement | null {
1✔
43
  const [currentPath, setCurrentPath] = useState('');
59✔
44
  const [homePath, setHomePath] = useState('');
59✔
45
  const [entries, setEntries] = useState<DirectoryEntry[]>([]);
59✔
46
  const [isLoading, setIsLoading] = useState(false);
59✔
47
  const [error, setError] = useState<string | null>(null);
59✔
48

49
  const fetchDirectory = useCallback(async (path: string) => {
59✔
50
    setIsLoading(true);
12✔
51
    setError(null);
12✔
52
    try {
12✔
53
      const result = await api.browseDirectory(path);
12✔
54
      setCurrentPath(result.path);
12✔
55
      setEntries(result.entries.filter((e) => e.isDirectory));
12✔
56
    } catch (err) {
12!
NEW
57
      const message = err instanceof Error ? err.message : 'Failed to load directory';
×
NEW
58
      setError(message);
×
59
    } finally {
12✔
60
      setIsLoading(false);
12✔
61
    }
12✔
62
  }, []);
59✔
63

64
  useEffect(() => {
59✔
65
    if (opened) {
20✔
66
      // Fetch user's home directory and browse it
67
      setIsLoading(true);
11✔
68
      void (async (): Promise<void> => {
11✔
69
        try {
11✔
70
          const homeResult = await api.getHomeDirectory();
11✔
71
          setHomePath(homeResult.path);
9✔
72
          await fetchDirectory(homeResult.path);
9✔
73
        } catch (err) {
11✔
74
          const message = err instanceof Error ? err.message : 'Failed to get home directory';
2!
75
          setError(message);
2✔
76
          setIsLoading(false);
2✔
77
        }
2✔
78
      })();
11✔
79
    }
11✔
80
  }, [opened, fetchDirectory]);
59✔
81

82
  const handleDirectoryClick = (entry: DirectoryEntry): void => {
59✔
83
    const newPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
2!
84
    void fetchDirectory(newPath);
2✔
85
  };
2✔
86

87
  const handleBreadcrumbClick = (path: string): void => {
59✔
88
    void fetchDirectory(path);
1✔
89
  };
1✔
90

91
  const handleSelect = (): void => {
59✔
92
    onSelect(currentPath);
3✔
93
    setCurrentPath('');
3✔
94
    setHomePath('');
3✔
95
  };
3✔
96

97
  const handleClose = (): void => {
59✔
98
    onClose();
1✔
99
    setCurrentPath('');
1✔
100
    setHomePath('');
1✔
101
  };
1✔
102

103
  if (!opened) {
59✔
104
    return null;
14✔
105
  }
14✔
106

107
  const breadcrumbs = pathToBreadcrumbs(currentPath, homePath);
45✔
108

109
  return (
45✔
110
    <Modal
45✔
111
      opened={opened}
45✔
112
      onClose={handleClose}
45✔
113
      title="Add Project"
45✔
114
      size="lg"
45✔
115
      centered
45✔
116
    >
117
      <Box data-testid="directory-browser">
45✔
118
        <Box mb="md">
45✔
119
          <Text size="xs" c="dimmed" mb={4}>
45✔
120
            Current path:
121
          </Text>
45✔
122
          <Text data-testid="current-path" fw={500}>
45✔
123
            {currentPath}
45✔
124
          </Text>
45✔
125
        </Box>
45✔
126

127
        <Breadcrumbs mb="md" separator="/">
45✔
128
          {breadcrumbs.map((crumb, index) => (
45✔
129
            <Anchor
20✔
130
              key={crumb.path}
20✔
131
              component="button"
20✔
132
              size="sm"
20✔
133
              onClick={() => { handleBreadcrumbClick(crumb.path); }}
20✔
134
              data-testid={index === 0 ? 'breadcrumb-home' : undefined}
20✔
135
            >
136
              {crumb.name}
20✔
137
            </Anchor>
20✔
138
          ))}
45✔
139
        </Breadcrumbs>
45✔
140

141
        <Box
45✔
142
          style={{
45✔
143
            border: '1px solid var(--mantine-color-dark-4)',
45✔
144
            borderRadius: 'var(--mantine-radius-sm)',
45✔
145
            maxHeight: 300,
45✔
146
            overflow: 'auto',
45✔
147
          }}
45✔
148
          p="xs"
45✔
149
        >
150
          {isLoading ? (
45✔
151
            <Center py="xl" data-testid="loading-indicator">
14✔
152
              <Loader size="sm" />
14✔
153
            </Center>
14✔
154
          ) : error ? (
31✔
155
            <Alert
2✔
156
              icon={<IconAlertCircle size={16} />}
2✔
157
              color="red"
2✔
158
              data-testid="error-message"
2✔
159
            >
160
              {error}
2✔
161
            </Alert>
2✔
162
          ) : entries.length === 0 ? (
29✔
163
            <Text size="sm" c="dimmed" ta="center" py="xl">
24✔
164
              No subdirectories
165
            </Text>
24✔
166
          ) : (
167
            <Stack gap={4}>
5✔
168
              {entries.map((entry) => (
5✔
169
                <UnstyledButton
5✔
170
                  key={entry.name}
5✔
171
                  onClick={() => { handleDirectoryClick(entry); }}
5✔
172
                  style={{
5✔
173
                    padding: '8px 12px',
5✔
174
                    borderRadius: 'var(--mantine-radius-sm)',
5✔
175
                    display: 'flex',
5✔
176
                    alignItems: 'center',
5✔
177
                    gap: 8,
5✔
178
                  }}
5✔
179
                  className="directory-item"
5✔
180
                >
181
                  <IconFolder size={18} style={{ color: 'var(--mantine-color-blue-4)' }} />
5✔
182
                  <Text size="sm">{entry.name}</Text>
5✔
183
                </UnstyledButton>
5✔
184
              ))}
5✔
185
            </Stack>
5✔
186
          )}
187
        </Box>
59✔
188

189
        <Group justify="flex-end" mt="lg">
59✔
190
          <Button variant="subtle" onClick={handleClose} data-testid="cancel-button">
59✔
191
            Cancel
192
          </Button>
59✔
193
          <Button onClick={handleSelect} data-testid="select-directory-button" disabled={isLoading}>
59✔
194
            Select This Directory
195
          </Button>
59✔
196
        </Group>
59✔
197
      </Box>
59✔
198
    </Modal>
59✔
199
  );
200
}
59✔
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

© 2026 Coveralls, Inc