Skip to content

Commit 9702910

Browse files
committed
feat: add "show org-scoped assets" toggle in Browse
- Introduced a toggle to show or hide organization-scoped assets in `Browse`, with state persisted in `localStorage`. - Added related tests to ensure correct behavior and persistence of settings. - Updated path generation logic to prepend `@` for org names across `
1 parent e228940 commit 9702910

6 files changed

Lines changed: 83 additions & 6 deletions

File tree

src/__tests__/lib/download-service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function buildUrl(
2828
): string {
2929
const typeDir = `${type}s`;
3030
const parts = ['assets', typeDir];
31-
if (org) parts.push(encodeURIComponent(org));
31+
if (org) parts.push(`@${encodeURIComponent(org)}`);
3232
parts.push(encodeURIComponent(name), encodeURIComponent(version), path);
3333
return `https://api.github.com/repos/EmergentSoftware/agentic-toolkit-registry/contents/${parts.join('/')}`;
3434
}

src/__tests__/lib/registry-client.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe('registry-client (Octokit-backed)', () => {
156156
expect(result.name).toBe('validate');
157157
expect(spy).toHaveBeenCalledWith(
158158
expect.objectContaining({
159-
path: 'assets/agents/agentic-toolkit/validate/1.1.0/manifest.json',
159+
path: 'assets/agents/@agentic-toolkit/validate/1.1.0/manifest.json',
160160
}),
161161
);
162162
});
@@ -198,7 +198,7 @@ describe('registry-client (Octokit-backed)', () => {
198198
expect(result).toBe(markdown);
199199
expect(spy).toHaveBeenCalledWith(
200200
expect.objectContaining({
201-
path: 'assets/agents/agentic-toolkit/validate/1.1.0/README.md',
201+
path: 'assets/agents/@agentic-toolkit/validate/1.1.0/README.md',
202202
}),
203203
);
204204
});

src/__tests__/routes/Browse.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function renderBrowse() {
8181
describe('BrowseRoute', () => {
8282
beforeEach(() => {
8383
window.localStorage.clear();
84+
window.localStorage.setItem('atk.browse.showOrgScoped', 'true');
8485
});
8586

8687
afterEach(() => {
@@ -234,6 +235,47 @@ describe('BrowseRoute', () => {
234235
expect(screen.getByTestId('asset-route')).toBeInTheDocument();
235236
});
236237

238+
it('hides org-scoped assets by default', () => {
239+
window.localStorage.removeItem('atk.browse.showOrgScoped');
240+
mockUseRegistry({ data: loadFixtureRegistry(), isSuccess: true });
241+
renderBrowse();
242+
243+
expect(screen.queryByTestId('browse-row-validate')).not.toBeInTheDocument();
244+
expect(screen.getByTestId('browse-row-dev-commands-rule')).toBeInTheDocument();
245+
});
246+
247+
it('shows org-scoped assets when the toggle is enabled', async () => {
248+
window.localStorage.removeItem('atk.browse.showOrgScoped');
249+
mockUseRegistry({ data: loadFixtureRegistry(), isSuccess: true });
250+
renderBrowse();
251+
252+
fireEvent.click(screen.getByTestId('toggle-show-org-scoped'));
253+
254+
await waitFor(() => {
255+
expect(screen.getByTestId('browse-row-validate')).toBeInTheDocument();
256+
});
257+
});
258+
259+
it('persists the show-org-scoped toggle to localStorage', async () => {
260+
window.localStorage.removeItem('atk.browse.showOrgScoped');
261+
mockUseRegistry({ data: loadFixtureRegistry(), isSuccess: true });
262+
renderBrowse();
263+
264+
fireEvent.click(screen.getByTestId('toggle-show-org-scoped'));
265+
266+
await waitFor(() => {
267+
expect(window.localStorage.getItem('atk.browse.showOrgScoped')).toBe('true');
268+
});
269+
});
270+
271+
it('restores the show-org-scoped toggle from localStorage', () => {
272+
window.localStorage.setItem('atk.browse.showOrgScoped', 'true');
273+
mockUseRegistry({ data: loadFixtureRegistry(), isSuccess: true });
274+
renderBrowse();
275+
276+
expect(screen.getByTestId('browse-row-validate')).toBeInTheDocument();
277+
});
278+
237279
it('persists column visibility to localStorage', async () => {
238280
mockUseRegistry({ data: loadFixtureRegistry(), isSuccess: true });
239281
renderBrowse();

src/lib/download-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ function buildFileUrl(ref: AssetRef, relativePath: string, options: DownloadAsse
152152
const repo = options.repo ?? DEFAULT_REPO;
153153
const typeDir = `${ref.type}s`;
154154
const parts = ['assets', typeDir];
155-
if (ref.org) parts.push(encodePathSegment(ref.org));
155+
if (ref.org) parts.push(encodePathSegment(`@${ref.org}`));
156156
parts.push(encodePathSegment(ref.name), encodePathSegment(ref.version));
157157
for (const segment of relativePath.split('/')) parts.push(encodePathSegment(segment));
158158
const base = `${GITHUB_API}/repos/${encodePathSegment(owner)}/${encodePathSegment(repo)}/contents/${parts.join('/')}`;

src/lib/registry-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export async function fetchRegistry(options: RegistryClientOptions): Promise<Reg
8282
function buildAssetManifestPath(ref: AssetManifestRef): string {
8383
const typeDir = `${ref.type}s`;
8484
const parts = ['assets', typeDir];
85-
if (ref.org) parts.push(ref.org);
85+
if (ref.org) parts.push(`@${ref.org}`);
8686
parts.push(ref.name, ref.version, 'manifest.json');
8787
return parts.join('/');
8888
}

src/routes/Browse.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const ALL_COLUMN_IDS = ['name', 'type', 'description', 'version', 'tags', 'tools
4747
type ColumnId = (typeof ALL_COLUMN_IDS)[number];
4848

4949
const COLUMN_VISIBILITY_STORAGE_KEY = 'atk.browse.columnVisibility';
50+
const SHOW_ORG_SCOPED_STORAGE_KEY = 'atk.browse.showOrgScoped';
5051

5152
interface BrowseCardListProps {
5253
isDownloading: (ref: { name: string; org?: string; type: AssetType; version: string }) => boolean;
@@ -95,6 +96,15 @@ export function BrowseRoute() {
9596
}
9697
}, [columnVisibility]);
9798

99+
const [showOrgScoped, setShowOrgScoped] = useState<boolean>(loadShowOrgScoped);
100+
useEffect(() => {
101+
try {
102+
window.localStorage.setItem(SHOW_ORG_SCOPED_STORAGE_KEY, JSON.stringify(showOrgScoped));
103+
} catch {
104+
/* ignore persistence errors */
105+
}
106+
}, [showOrgScoped]);
107+
98108
const assets = useMemo<RegistryAsset[]>(() => data?.assets ?? [], [data]);
99109

100110
const { allOrgs, allTags, allTools } = useMemo(() => {
@@ -116,6 +126,7 @@ export function BrowseRoute() {
116126

117127
const rows = useMemo<BrowseRow[]>(() => {
118128
const filtered = assets.filter((asset) => {
129+
if (!showOrgScoped && orgFilter.size === 0 && asset.org) return false;
119130
if (typeFilter.size > 0 && !typeFilter.has(asset.type)) return false;
120131
if (tagFilter.size > 0 && !asset.tags.some((t) => tagFilter.has(t))) return false;
121132
const latest = asset.versions[asset.latest];
@@ -135,7 +146,7 @@ export function BrowseRoute() {
135146
}
136147

137148
return ordered.map(toRow);
138-
}, [assets, typeFilter, tagFilter, toolFilter, orgFilter, search]);
149+
}, [assets, typeFilter, tagFilter, toolFilter, orgFilter, search, showOrgScoped]);
139150

140151
const columns = useMemo<ColumnDef<BrowseRow>[]>(
141152
() => [
@@ -322,6 +333,18 @@ export function BrowseRoute() {
322333
options={allOrgs}
323334
selected={orgFilter}
324335
/>
336+
<label
337+
className='flex cursor-pointer items-center gap-2 rounded-md border border-input bg-transparent px-3 py-1.5 text-sm text-foreground hover:border-primary/60 hover:bg-accent'
338+
htmlFor='toggle-show-org-scoped'
339+
>
340+
<Checkbox
341+
checked={showOrgScoped}
342+
data-testid='toggle-show-org-scoped'
343+
id='toggle-show-org-scoped'
344+
onChange={(event) => setShowOrgScoped(event.target.checked)}
345+
/>
346+
<span>Show org-scoped assets</span>
347+
</label>
325348
{hasActiveFilters ? (
326349
<Button
327350
className='ml-auto'
@@ -647,6 +670,18 @@ function loadColumnVisibility(): VisibilityState {
647670
}
648671
}
649672

673+
function loadShowOrgScoped(): boolean {
674+
if (typeof window === 'undefined') return false;
675+
try {
676+
const raw = window.localStorage.getItem(SHOW_ORG_SCOPED_STORAGE_KEY);
677+
if (raw === null) return false;
678+
const parsed: unknown = JSON.parse(raw);
679+
return typeof parsed === 'boolean' ? parsed : false;
680+
} catch {
681+
return false;
682+
}
683+
}
684+
650685
function parseSort(value: string): SortingState {
651686
if (!value) return [];
652687
const [id, dir] = value.split(':');

0 commit comments

Comments
 (0)