Skip to content

Commit df4f69c

Browse files
authored
feat(projects): add organizationId and includeMembers parameters to useProjects hook (#607)
- Add optional organizationId parameter for filtering projects by organization - Add optional includeMembers parameter - Implement parameter-based caching with unique cache keys
1 parent 6ef5b6c commit df4f69c

File tree

4 files changed

+98
-12
lines changed

4 files changed

+98
-12
lines changed

packages/core/src/projects/projects.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,41 @@ describe('projects', () => {
3636

3737
const result = await resolveProjects(instance)
3838
expect(result).toEqual(projects)
39-
expect(list).toHaveBeenCalledWith({includeMembers: false})
39+
expect(list).toHaveBeenCalledWith({includeMembers: false, organizationId: undefined})
40+
})
41+
})
42+
43+
describe('projects cache key generation', () => {
44+
it('generates correct cache keys for different parameter combinations', async () => {
45+
// Test the getKey function directly by creating a mock store
46+
const mockGetKey = (
47+
_instance: SanityInstance,
48+
options?: {organizationId?: string; includeMembers?: boolean},
49+
) => {
50+
const orgKey = options?.organizationId ? `:org:${options.organizationId}` : ''
51+
const membersKey = options?.includeMembers === false ? ':no-members' : ''
52+
return `projects${orgKey}${membersKey}`
53+
}
54+
55+
const mockInstance = {} as SanityInstance
56+
57+
// Test default behavior (no options)
58+
const defaultKey = mockGetKey(mockInstance)
59+
expect(defaultKey).toBe('projects')
60+
61+
// Test with organizationId only
62+
const orgKey = mockGetKey(mockInstance, {organizationId: 'org123'})
63+
expect(orgKey).toBe('projects:org:org123')
64+
65+
// Test with includeMembers: false only
66+
const noMembersKey = mockGetKey(mockInstance, {includeMembers: false})
67+
expect(noMembersKey).toBe('projects:no-members')
68+
69+
// Test with both parameters
70+
const bothKey = mockGetKey(mockInstance, {
71+
organizationId: 'org123',
72+
includeMembers: false,
73+
})
74+
expect(bothKey).toBe('projects:org:org123:no-members')
4075
})
4176
})

packages/core/src/projects/projects.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,25 @@ const API_VERSION = 'v2025-02-19'
77

88
const projects = createFetcherStore({
99
name: 'Projects',
10-
getKey: () => 'projects',
11-
fetcher: (instance) => () =>
10+
getKey: (_instance, options?: {organizationId?: string; includeMembers?: boolean}) => {
11+
const orgKey = options?.organizationId ? `:org:${options.organizationId}` : ''
12+
const membersKey = options?.includeMembers === false ? ':no-members' : ''
13+
return `projects${orgKey}${membersKey}`
14+
},
15+
fetcher: (instance) => (options?: {organizationId?: string; includeMembers?: boolean}) =>
1216
getClientState(instance, {
1317
apiVersion: API_VERSION,
1418
scope: 'global',
19+
requestTagPrefix: 'sanity.sdk.projects',
1520
}).observable.pipe(
16-
switchMap((client) => client.observable.projects.list({includeMembers: false})),
21+
switchMap((client) => {
22+
const organizationId = options?.organizationId
23+
return client.observable.projects.list({
24+
// client method has a type that expects false | undefined
25+
includeMembers: !options?.includeMembers ? false : undefined,
26+
organizationId,
27+
})
28+
}),
1729
),
1830
})
1931

packages/react/src/hooks/projects/useProjects.test.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ describe('useProjects', () => {
5959
// Mock instance for the test call
6060
const mockInstance = {} as SanityInstance // Use specific type
6161

62-
// Call the shouldSuspend function
63-
const result = shouldSuspend(mockInstance)
62+
// Call the shouldSuspend function with both required parameters
63+
const result = shouldSuspend(mockInstance, undefined) // Pass undefined for options
6464

6565
// Assert that getProjectsState was called with the correct arguments
6666
const mockGetProjectsState = getProjectsState as ReturnType<typeof vi.fn>
67-
expect(mockGetProjectsState).toHaveBeenCalledWith(mockInstance)
67+
expect(mockGetProjectsState).toHaveBeenCalledWith(mockInstance, undefined)
6868

6969
// Assert that getCurrent was called on the result of getProjectsState
7070
expect(mockGetProjectsState.mock.results.length).toBeGreaterThan(0)
@@ -74,4 +74,39 @@ describe('useProjects', () => {
7474
// Assert the result of shouldSuspend based on the mocked getCurrent value
7575
expect(result).toBe(true) // Since getCurrent is mocked to return undefined
7676
})
77+
78+
it('should call createStateSourceHook with correct getState function signature', async () => {
79+
await import('./useProjects')
80+
81+
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
82+
expect(mockCreateStateSourceHook).toHaveBeenCalled()
83+
84+
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
85+
const getState = createStateSourceHookArgs.getState
86+
87+
// Test that getState can handle the new options parameter
88+
const mockInstance = {} as SanityInstance
89+
const mockOptions = {organizationId: 'org123', includeMembers: false}
90+
91+
// This should not throw
92+
expect(() => getState(mockInstance, mockOptions)).not.toThrow()
93+
})
94+
95+
it('should handle different parameter combinations in shouldSuspend', async () => {
96+
await import('./useProjects')
97+
98+
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
99+
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
100+
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
101+
102+
const mockInstance = {} as SanityInstance
103+
104+
// Test with different options
105+
expect(() => shouldSuspend(mockInstance, undefined)).not.toThrow()
106+
expect(() => shouldSuspend(mockInstance, {organizationId: 'org123'})).not.toThrow()
107+
expect(() => shouldSuspend(mockInstance, {includeMembers: false})).not.toThrow()
108+
expect(() =>
109+
shouldSuspend(mockInstance, {organizationId: 'org123', includeMembers: false}),
110+
).not.toThrow()
111+
})
77112
})

packages/react/src/hooks/projects/useProjects.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,20 @@ type UseProjects = {
3030
* )
3131
* ```
3232
*/
33-
(): ProjectWithoutMembers[]
33+
(options?: {organizationId?: string; includeMembers?: true}): SanityProject[]
34+
(options: {organizationId?: string; includeMembers?: false}): ProjectWithoutMembers[]
3435
}
3536

3637
/**
3738
* @public
3839
* @function
3940
*/
4041
export const useProjects: UseProjects = createStateSourceHook({
41-
// remove `undefined` since we're suspending when that is the case
42-
getState: getProjectsState as (instance: SanityInstance) => StateSource<ProjectWithoutMembers[]>,
43-
shouldSuspend: (instance) => getProjectsState(instance).getCurrent() === undefined,
42+
getState: getProjectsState as (
43+
instance: SanityInstance,
44+
options?: {organizationId?: string; includeMembers?: boolean},
45+
) => StateSource<SanityProject[] | ProjectWithoutMembers[]>,
46+
shouldSuspend: (instance, options) =>
47+
getProjectsState(instance, options).getCurrent() === undefined,
4448
suspender: resolveProjects,
45-
})
49+
}) as UseProjects

0 commit comments

Comments
 (0)