diff --git a/src/App.vue b/src/App.vue index fb139bb..adbe941 100644 --- a/src/App.vue +++ b/src/App.vue @@ -35,9 +35,82 @@ + + +
+

Page Types

+
+ + + + Add +
+ +

No page types configured

+
+ + +
+

Collection Keys

+
+ + + + Add +
+ +

No collection keys configured

+

Utilities

@@ -52,6 +125,7 @@ diff --git a/src/components/Btn.vue b/src/components/Btn.vue index 6f18bbc..ff088ea 100644 --- a/src/components/Btn.vue +++ b/src/components/Btn.vue @@ -30,7 +30,7 @@ withDefaults( */ href?: string disabled?: boolean - status?: 'secondary' + status?: 'secondary' | 'tertiary' }>(), { tag: 'button', @@ -56,7 +56,7 @@ withDefaults( color: var(--btn-color); text-decoration: none; padding: 0.75rem 1rem; - border-radius: 0.5rem; + border-radius: 0.3125rem; cursor: pointer; outline-offset: 4px; height: var(--btn-height); @@ -73,6 +73,14 @@ withDefaults( position: relative; &--secondary { + --btn-background-color: var(--text-primary); + --btn-border-color: var(--text-primary); + --btn-color: #fff; + --btn-hover-background-color: var(--butter-dark); + --btn-hover-border-color: var(--butter-dark); + } + + &--tertiary { --btn-background-color: #fff; --btn-border-color: var(--butter-yellow); --btn-color: var(--butter-dark); diff --git a/src/components/Features/SearchContent.vue b/src/components/Features/SearchContent.vue index 3f3117d..71a3b5c 100644 --- a/src/components/Features/SearchContent.vue +++ b/src/components/Features/SearchContent.vue @@ -3,54 +3,75 @@ title="Search Content" description="Search through pages, blog posts, or collections for content matching your search term. Results will highlight exactly where matches were found." > - Search everything - -
- -
- - - + +
+ Search Scopes + + + + + +
+
Page Types
+
+ +
-
- - - - - + +
+
Collection Keys
+
+ +
+
- - - +
- +

+ No page types or collection keys configured. Configure them in API Configuration above. +

+
+ - + Reset @@ -81,8 +102,9 @@
Found {{ results.length }} - {{ pluralize(results.length, getScopeItemName(), getScopeItemNamePlural()) }} containing - "{{ searchTerm }}{{ + searchTerm + }}" with {{ totalMatches }} total {{ pluralize(totalMatches, 'match', 'matches') }}
@@ -139,17 +161,17 @@ import TextInput from '../TextInput.vue' import Btn from '../Btn.vue' import InfoBanner from '../InfoBanner.vue' import Chip from '../Chip.vue' -import ComingSoon from '../ComingSoon.vue' import { searchContent } from '@/features/searchContent' import type { AsyncReturnType } from 'type-fest' const store = useStore() -const searchScope = ref<'pages' | 'blog' | 'collections'>('pages') -const pageType = ref('') -const collectionKey = ref('') +const includeBlog = computed({ + get: () => store.selectedScopes.blog, + set: (val: boolean) => { + store.selectedScopes = { ...store.selectedScopes, blog: val } + }, +}) const searchTerm = ref('') -const showMissingPageTypeError = ref(false) -const showMissingCollectionKeyError = ref(false) const showMissingSearchTermError = ref(false) const isLoading = ref(false) const statusMessage = ref('') @@ -184,11 +206,7 @@ function resetSearch(): void { failedResource.value = null failedError.value = null statusMessage.value = '' - pageType.value = '' - collectionKey.value = '' searchTerm.value = '' - showMissingPageTypeError.value = false - showMissingCollectionKeyError.value = false showMissingSearchTermError.value = false } @@ -202,50 +220,31 @@ function setStatus( isLoading.value = loading } -function getScopeItemName(): string { - switch (searchScope.value) { - case 'pages': - return 'page' - case 'blog': - return 'blog post' - case 'collections': - return 'collection item' - default: - return 'item' - } -} - -function getScopeItemNamePlural(): string { - switch (searchScope.value) { - case 'pages': - return 'pages' - case 'blog': - return 'blog posts' - case 'collections': - return 'collection items' - default: - return 'items' - } -} - function getResultSourceBadge( result: AsyncReturnType['results'][number], ): string { - if (searchScope.value === 'pages' && result.sourceType) { - return `Page (${result.sourceType})` - } else if (searchScope.value === 'collections' && result.sourceType) { - return `Collection (${result.sourceType})` - } else if (searchScope.value === 'blog') { + if (result.sourceType === 'Blog') { return 'Blog' } - return 'Unknown' + return `${result.sourceType}` +} + +function togglePageType(pageType: string): void { + const index = store.selectedScopes.pageTypes.indexOf(pageType) + const newPageTypes = + index > -1 + ? store.selectedScopes.pageTypes.filter((pt) => pt !== pageType) + : [...store.selectedScopes.pageTypes, pageType] + store.selectedScopes = { ...store.selectedScopes, pageTypes: newPageTypes } } -function parseCommaSeparatedInput(input: string): string[] { - return input - .split(',') - .map((item) => item.trim()) - .filter((item) => item.length > 0) +function toggleCollectionKey(collectionKey: string): void { + const index = store.selectedScopes.collectionKeys.indexOf(collectionKey) + const newCollectionKeys = + index > -1 + ? store.selectedScopes.collectionKeys.filter((ck) => ck !== collectionKey) + : [...store.selectedScopes.collectionKeys, collectionKey] + store.selectedScopes = { ...store.selectedScopes, collectionKeys: newCollectionKeys } } // Main search execution @@ -264,34 +263,20 @@ async function executeSearch(): Promise { } showMissingSearchTermError.value = !searchTerm.value - showMissingPageTypeError.value = searchScope.value === 'pages' && !pageType.value - showMissingCollectionKeyError.value = searchScope.value === 'collections' && !collectionKey.value - - if ( - showMissingSearchTermError.value || - showMissingPageTypeError.value || - showMissingCollectionKeyError.value - ) - return + + if (showMissingSearchTermError.value) return isLoading.value = true setStatus('Searching...', 'info', true) try { - const pageTypesArray = - searchScope.value === 'pages' ? parseCommaSeparatedInput(pageType.value) : undefined - const collectionKeysArray = - searchScope.value === 'collections' - ? parseCommaSeparatedInput(collectionKey.value) - : undefined - const searchResponse = await searchContent( - searchScope.value, searchTermValue, token, store.includePreview, - pageTypesArray, - collectionKeysArray, + store.selectedScopes.pageTypes, + store.selectedScopes.collectionKeys, + includeBlog.value, ) if (!searchResponse.success) { @@ -305,13 +290,13 @@ async function executeSearch(): Promise { if (searchResponse.results.length === 0) { setStatus( - `No matches found for "${searchTermValue}" in ${searchResponse.totalItems} ${pluralize(searchResponse.totalItems!, getScopeItemName(), getScopeItemNamePlural())}`, + `No matches found for "${searchTermValue}" in ${searchResponse.totalItems} selected items`, 'info', false, ) } else { setStatus( - `Found ${searchResponse.results.length} ${pluralize(searchResponse.results.length, getScopeItemName(), getScopeItemNamePlural())} with matches out of ${searchResponse.totalItems} total ${pluralize(searchResponse.totalItems!, getScopeItemName(), getScopeItemNamePlural())}`, + `Found ${searchResponse.results.length} items with matches out of ${searchResponse.totalItems} total selected items`, 'success', false, ) @@ -339,46 +324,67 @@ function getResultMatchCount( diff --git a/src/components/WhatsNew.vue b/src/components/WhatsNew.vue index dcdfc66..cb51531 100644 --- a/src/components/WhatsNew.vue +++ b/src/components/WhatsNew.vue @@ -41,12 +41,13 @@ const showModal = ref(false) const features: Feature[] = [ { - id: 'search-content-multi-types', + id: 'search-content-multi-scope', type: 'improvement', - title: 'Search Content utility now supports multiple page types and collection types', + title: + 'Define and store your known page types & collections keys, and search across them simultaneously', description: - 'The Search Content utility has been improved to allow searching across multiple page types and collection types at once, making it easier to find content that may be spread across different types. Simply use a comma-separated list of page type and collection keys in the search input to search across them all simultaneously.', - utcDatetimeAdded: new Date('2026-02-08T14:00:00Z'), + 'You can now use the API Configuration UI to store your known page types and collection keys. These will be remembered on your device. Use these to use the new checkbox-UI in the Search Content utility to search across all your content simultaneously, instead of having to search each page type and collection separately.', + utcDatetimeAdded: new Date('2026-02-08T20:30:00Z'), }, { id: 'search-content-alt-text', diff --git a/src/features/searchContent.spec.ts b/src/features/searchContent.spec.ts index a72f979..aa726f7 100644 --- a/src/features/searchContent.spec.ts +++ b/src/features/searchContent.spec.ts @@ -1,20 +1,34 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { searchContent } from './searchContent' -import * as pagesModule from '@/core/pages' -import * as postsModule from '@/core/posts' -import * as collectionsModule from '@/core/collections' -vi.mock('@/core/pages') -vi.mock('@/core/posts') -vi.mock('@/core/collections') +const { + getAllPages: mockGetAllPages, + getAllPosts: mockGetAllPosts, + getAllCollections: mockGetAllCollections, +} = vi.hoisted(() => ({ + getAllPages: vi.fn(), + getAllPosts: vi.fn(), + getAllCollections: vi.fn(), +})) -const mockGetAllPages = pagesModule.getAllPages as ReturnType -const mockGetAllPosts = postsModule.getAllPosts as ReturnType -const mockGetAllCollections = collectionsModule.getAllCollections as ReturnType +vi.mock('@/core/pages', () => ({ + getAllPages: mockGetAllPages, +})) + +vi.mock('@/core/posts', () => ({ + getAllPosts: mockGetAllPosts, +})) + +vi.mock('@/core/collections', () => ({ + getAllCollections: mockGetAllCollections, +})) describe('searchContent', () => { beforeEach(() => { - vi.clearAllMocks() + // Reset all mocks before each test + mockGetAllPages.mockReset() + mockGetAllPosts.mockReset() + mockGetAllCollections.mockReset() }) afterEach(() => { @@ -23,1257 +37,212 @@ describe('searchContent', () => { describe('Input validation', () => { it('should handle empty search string', async () => { - const result = await searchContent('pages', '', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(0) - expect(result.totalItems).toBe(0) - expect(mockGetAllPages).not.toHaveBeenCalled() - }) + mockGetAllPages.mockResolvedValueOnce([]) + mockGetAllPosts.mockResolvedValueOnce([]) + mockGetAllCollections.mockResolvedValueOnce([]) - it('should handle whitespace-only search string', async () => { - const result = await searchContent('pages', ' ', 'test-token', false, ['landing_page']) + const result = await searchContent('', 'test-token', false, ['landing_page'], [], true) expect(result.success).toBe(true) expect(result.results).toHaveLength(0) - expect(result.totalItems).toBe(0) - expect(mockGetAllPages).not.toHaveBeenCalled() }) - it('should trim search string', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'test', - name: 'keyword', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', ' keyword ', 'test-token', false, [ - 'landing_page', - ]) + it('should return error when no scopes are selected', async () => { + const result = await searchContent('test', 'test-token', false, [], [], false) - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) + expect(result.success).toBe(false) + expect(result.error).toContain('at least one') }) }) describe('Pages scope', () => { - it('should search pages and return matching results', async () => { - const mockPages = [ + it('should search pages', async () => { + mockGetAllPages.mockImplementation(async () => [ { id: '1', slug: 'about', - name: 'About Us', - page_type: 'landing_page', - published: '2023-01-01', - }, - { - id: '2', - slug: 'contact', - name: 'Contact Page', - page_type: 'landing_page', - published: '2023-01-02', - }, - ] - - mockGetAllPages.mockResolvedValueOnce(mockPages) - - const result = await searchContent('pages', 'Contact', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.totalItems).toBe(2) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.slug).toBe('contact') - expect(result.results[0]!.title).toBe('Contact Page') - expect(result.results[0]!.sourceType).toBe('landing_page') - }) - - it('should return error when pageTypes is missing or empty', async () => { - const result = await searchContent('pages', 'test', 'test-token', false) - - expect(result.success).toBe(false) - expect(result.error).toContain('Invalid search scope or missing required parameter') - }) - - it('should handle no matches in pages', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'First Page', + name: 'TestAbout', page_type: 'landing_page', published: '2023-01-01', }, ]) + mockGetAllPosts.mockImplementation(async () => []) + mockGetAllCollections.mockImplementation(async () => []) - const result = await searchContent('pages', 'xyz', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(0) - expect(result.totalItems).toBe(1) - }) - - it('should include preview parameter when preview is true', async () => { - mockGetAllPages.mockResolvedValueOnce([]) - - await searchContent('pages', 'test', 'test-token', true, ['landing_page']) - - expect(mockGetAllPages).toHaveBeenCalledWith({ - token: 'test-token', - pageType: 'landing_page', - preview: true, - }) - }) - - it('should handle getAllPages error', async () => { - mockGetAllPages.mockRejectedValueOnce(new Error('API Error')) - - const result = await searchContent('pages', 'test', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(false) - expect(result.error).toContain('API Error') - }) - - it('should search multiple page types and combine results', async () => { - const mockPagesType1 = [ - { - id: '1', - slug: 'about', - name: 'About Us', - page_type: 'landing_page', - published: '2023-01-01', - }, - ] - const mockPagesType2 = [ - { - id: '2', - slug: 'pricing', - name: 'Pricing Page', - page_type: 'service_page', - published: '2023-01-02', - }, - ] - - mockGetAllPages.mockResolvedValueOnce(mockPagesType1) - mockGetAllPages.mockResolvedValueOnce(mockPagesType2) - - const result = await searchContent('pages', 'Page', 'test-token', false, [ - 'landing_page', - 'service_page', - ]) + const result = await searchContent( + 'TestAbout', + 'test-token', + false, + ['landing_page'], + [], + false, + ) expect(result.success).toBe(true) - expect(result.totalItems).toBe(2) - expect(result.results).toHaveLength(2) - expect(mockGetAllPages).toHaveBeenCalledTimes(2) - expect(result.results[0]!.sourceType).toBe('landing_page') - expect(result.results[1]!.sourceType).toBe('service_page') - expect(mockGetAllPages).toHaveBeenCalledWith({ - token: 'test-token', - pageType: 'landing_page', - preview: false, - }) - expect(mockGetAllPages).toHaveBeenCalledWith({ - token: 'test-token', - pageType: 'service_page', - preview: false, - }) + expect(result.results).toHaveLength(1) + expect(result.results[0]!.slug).toBe('about') }) }) describe('Blog scope', () => { - it('should search blog posts and return matching results', async () => { - const mockPosts = [ - { - id: '1', - slug: 'first-post', - title: 'First Blog Post', - published: '2023-01-01', - }, - { - id: '2', - slug: 'second-post', - title: 'Second Blog Post', - published: '2023-01-02', - }, - ] - - mockGetAllPosts.mockResolvedValueOnce(mockPosts) - - const result = await searchContent('blog', 'Blog', 'test-token', false) - - expect(result.success).toBe(true) - expect(result.totalItems).toBe(2) - expect(result.results).toHaveLength(2) - expect(result.results[0]!.title).toBe('First Blog Post') - }) - - it('should handle no matches in blog posts', async () => { - mockGetAllPosts.mockResolvedValueOnce([ + it('should search blog', async () => { + mockGetAllPages.mockImplementation(async () => []) + mockGetAllPosts.mockImplementation(async () => [ { id: '1', slug: 'post-1', - title: 'Post Title', + title: 'TestBlogPost', published: '2023-01-01', }, ]) + mockGetAllCollections.mockImplementation(async () => []) - const result = await searchContent('blog', 'nonexistent', 'test-token', false) + const result = await searchContent('TestBlogPost', 'test-token', false, [], [], true) expect(result.success).toBe(true) - expect(result.results).toHaveLength(0) - }) - - it('should handle getAllPosts error', async () => { - mockGetAllPosts.mockRejectedValueOnce(new Error('Network Error')) - - const result = await searchContent('blog', 'test', 'test-token', false) - - expect(result.success).toBe(false) - expect(result.error).toContain('Network Error') + expect(result.results).toHaveLength(1) + expect(result.results[0]!.slug).toBe('post-1') }) }) describe('Collections scope', () => { - it('should search collections and return matching results', async () => { - const mockCollections = [ - { - id: 1, - slug: 'product-1', - name: 'Product One', - }, - { - id: 2, - slug: 'product-2', - name: 'Product Two', - }, - ] - - mockGetAllCollections.mockResolvedValueOnce(mockCollections) - - const result = await searchContent('collections', 'Product', 'test-token', false, undefined, [ - 'products', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(2) - expect(result.results[0]!.slug).toBe('product-1') - expect(result.results[0]!.sourceType).toBe('products') - }) - - it('should return error when collectionKeys is missing or empty', async () => { - const result = await searchContent('collections', 'test', 'test-token', false) - - expect(result.success).toBe(false) - expect(result.error).toContain('Invalid search scope or missing required parameter') - }) - - it('should handle getAllCollections error', async () => { - mockGetAllCollections.mockRejectedValueOnce(new Error('Collection Error')) - - const result = await searchContent('collections', 'test', 'test-token', false, undefined, [ - 'products', - ]) - - expect(result.success).toBe(false) - expect(result.error).toContain('Collection Error') - }) - - it('should search multiple collection types and combine results', async () => { - const mockCollectionsType1 = [ + it('should search collections', async () => { + mockGetAllPages.mockImplementation(async () => []) + mockGetAllPosts.mockImplementation(async () => []) + mockGetAllCollections.mockImplementation(async () => [ { id: 1, - slug: 'product-1', - name: 'Product One', - }, - ] - const mockCollectionsType2 = [ - { - id: 2, - slug: 'team-member-1', - name: 'Team Member One', - }, - ] - - mockGetAllCollections.mockResolvedValueOnce(mockCollectionsType1) - mockGetAllCollections.mockResolvedValueOnce(mockCollectionsType2) - - const result = await searchContent('collections', 'One', 'test-token', false, undefined, [ - 'products', - 'team_members', - ]) - - expect(result.success).toBe(true) - expect(result.totalItems).toBe(2) - expect(result.results).toHaveLength(2) - expect(mockGetAllCollections).toHaveBeenCalledTimes(2) - expect(result.results[0]!.sourceType).toBe('products') - expect(result.results[1]!.sourceType).toBe('team_members') - expect(mockGetAllCollections).toHaveBeenCalledWith({ - token: 'test-token', - collectionType: 'products', - preview: false, - }) - expect(mockGetAllCollections).toHaveBeenCalledWith({ - token: 'test-token', - collectionType: 'team_members', - preview: false, - }) - }) - }) - - describe('Case-insensitive matching', () => { - it('should match lowercase search term against uppercase text', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'UPPERCASE TEXT', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'uppercase', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.matches).toHaveLength(1) - }) - - it('should match uppercase search term against lowercase text', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'lowercase text', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'LOWERCASE', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - - it('should match mixed case search term', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'MixedCase Content', - page_type: 'landing_page', - published: '2023-01-01', + slug: 'item-1', + name: 'TestCollection', }, ]) - const result = await searchContent('pages', 'mixed', 'test-token', false, ['landing_page']) + const result = await searchContent( + 'TestCollection', + 'test-token', + false, + [], + ['items'], + false, + ) expect(result.success).toBe(true) expect(result.results).toHaveLength(1) + expect(result.results[0]!.slug).toBe('item-1') }) }) - describe('Whitespace normalization', () => { - it('should match text with non-breaking spaces', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Hello\u00A0World', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'hello world', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - - it('should match text with HTML nbsp entities', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Test Content', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'test content', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - - it('should normalize multiple consecutive spaces', async () => { - mockGetAllPages.mockResolvedValueOnce([ + describe('Multiple scopes combined', () => { + it('should handle combined scopes', async () => { + mockGetAllPages.mockImplementation(async () => [ { id: '1', - slug: 'page-1', - name: 'Text with many spaces', + slug: 'test-page', + name: 'Test Page', page_type: 'landing_page', published: '2023-01-01', }, ]) + mockGetAllPosts.mockImplementation(async () => []) + mockGetAllCollections.mockImplementation(async () => []) - const result = await searchContent('pages', 'with many', 'test-token', false, [ - 'landing_page', - ]) + const result = await searchContent('Test', 'test-token', false, ['landing_page'], [], true) expect(result.success).toBe(true) expect(result.results).toHaveLength(1) - }) - - it('should use normalized text in snippets', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Before keyword after', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - // Snippet should contain normalized spaces, not   - expect(result.results[0]!.matches[0]!.value).toContain('Before keyword after') - expect(result.results[0]!.matches[0]!.value).not.toContain(' ') + expect(result.results[0]!.sourceType).toBe('landing_page') }) }) - describe('Alphabetical sorting', () => { - it('should sort results alphabetically by slug', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '3', - slug: 'zebra-page', - name: 'Zebra Content', - page_type: 'landing_page', - published: '2023-01-01', - }, - { - id: '1', - slug: 'apple-page', - name: 'Apple Content', - page_type: 'landing_page', - published: '2023-01-02', - }, - { - id: '2', - slug: 'banana-page', - name: 'Banana Content', - page_type: 'landing_page', - published: '2023-01-03', - }, - ]) + describe('Error handling', () => { + it('should handle API errors gracefully', async () => { + mockGetAllPages.mockRejectedValueOnce(new Error('API Error')) + mockGetAllPosts.mockImplementation(async () => []) + mockGetAllCollections.mockImplementation(async () => []) - const result = await searchContent('pages', 'content', 'test-token', false, ['landing_page']) + const result = await searchContent('test', 'test-token', false, ['landing_page'], [], false) - expect(result.success).toBe(true) - expect(result.results[0]!.slug).toBe('apple-page') - expect(result.results[1]!.slug).toBe('banana-page') - expect(result.results[2]!.slug).toBe('zebra-page') + expect(result.success).toBe(false) + expect(result.error).toContain('API Error') }) }) - describe('Multiple occurrences optimization', () => { - it('should consolidate multiple occurrences into single match with count', async () => { - mockGetAllPages.mockResolvedValueOnce([ + describe('Multiple matches in single field', () => { + it('should handle multiple occurrences of search term in same field', async () => { + mockGetAllPages.mockImplementation(async () => [ { id: '1', - slug: 'page-1', - name: 'test test test test test', + slug: 'tutorial', + name: 'Testing testing testing services', page_type: 'landing_page', published: '2023-01-01', }, ]) + mockGetAllPosts.mockImplementation(async () => []) + mockGetAllCollections.mockImplementation(async () => []) - const result = await searchContent('pages', 'test', 'test-token', false, ['landing_page']) + const result = await searchContent( + 'testing', + 'test-token', + false, + ['landing_page'], + [], + false, + ) expect(result.success).toBe(true) expect(result.results).toHaveLength(1) - expect(result.results[0]!.matches).toHaveLength(1) - expect(result.results[0]!.matches[0]!.count).toBe(5) + expect(result.results[0]!.matches[0]!.count).toBe(3) expect(result.results[0]!.matches[0]!.path).toContain('occurrences') }) + }) - it('should limit snippet generation for many occurrences', async () => { - const manyOccurrences = 'keyword '.repeat(100) - mockGetAllPages.mockResolvedValueOnce([ + describe('Array content searching', () => { + it('should search through array content', async () => { + mockGetAllPages.mockImplementation(async () => [ { id: '1', - slug: 'page-1', - name: manyOccurrences, + slug: 'page-with-array', + name: 'Page', page_type: 'landing_page', published: '2023-01-01', + tags: ['SearchableTag', 'other'], }, ]) + mockGetAllPosts.mockImplementation(async () => []) + mockGetAllCollections.mockImplementation(async () => []) - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches[0]!.count).toBe(100) - // Should still return a valid snippet - expect(result.results[0]!.matches[0]!.value).toBeTruthy() - expect(result.results[0]!.matches[0]!.value).toContain('keyword') - }) - }) - - describe('Circular reference protection', () => { - it('should handle circular references without infinite loop', async () => { - const circularObj: Record = { - id: '1', - slug: 'circular', - name: 'Circular Reference Test', - page_type: 'landing_page', - published: '2023-01-01', - } - circularObj.self = circularObj - circularObj.nested = { parent: circularObj } - - mockGetAllPages.mockResolvedValueOnce([circularObj]) - - const result = await searchContent('pages', 'Circular', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.title).toBe('Circular Reference Test') - }) - - it('should handle self-referencing arrays', async () => { - const circularArray: Record = { - id: '1', - slug: 'array-circular', - name: 'Array Test', - page_type: 'landing_page', - published: '2023-01-01', - items: [], - } - ;(circularArray.items as Array>).push(circularArray) - - mockGetAllPages.mockResolvedValueOnce([circularArray]) - - const result = await searchContent('pages', 'Array', 'test-token', false, ['landing_page']) + const result = await searchContent( + 'SearchableTag', + 'test-token', + false, + ['landing_page'], + [], + false, + ) expect(result.success).toBe(true) expect(result.results).toHaveLength(1) }) }) - describe('Depth limit protection', () => { - it('should stop recursion at depth 10', async () => { - // Create deeply nested object (12 levels) - let deepObj: Record = { value: 'deep keyword' } - for (let i = 0; i < 12; i++) { - deepObj = { level: deepObj } - } - - mockGetAllPages.mockResolvedValueOnce([ + describe('Numeric and boolean searches', () => { + it('should search numeric and boolean values', async () => { + mockGetAllPages.mockImplementation(async () => [ { id: '1', - slug: 'deep', - name: 'Deep Object', + slug: 'page-numeric', + name: 'Page', page_type: 'landing_page', published: '2023-01-01', - data: deepObj, + views: 12345, + featured: true, }, ]) + mockGetAllPosts.mockImplementation(async () => []) + mockGetAllCollections.mockImplementation(async () => []) - const result = await searchContent('pages', 'deep', 'test-token', false, ['landing_page']) - - // Should find match in name field but not in deeply nested value - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.title).toBe('Deep Object') - }) - - it('should handle depth 10 structures correctly', async () => { - // Create object at depth 9 (will be depth 10 when counting from root item) - // Root item = depth 0, data = depth 1, nested = depth 2, etc. - let obj: Record = { target: 'findme' } - for (let i = 0; i < 8; i++) { - obj = { nested: obj } - } - - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page', - name: 'Page', - page_type: 'landing_page', - published: '2023-01-01', - data: obj, - }, - ]) - - const result = await searchContent('pages', 'findme', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - // Should find it at depth 10 (root=0, data=1, nested=2...10) - expect(result.results).toHaveLength(1) - }) - }) - - describe('Nested object searching', () => { - it('should find matches in nested objects', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Page Name', - page_type: 'landing_page', - published: '2023-01-01', - fields: { - heading: 'Searchable Content', - subheading: 'More nested content', - }, - }, - ]) - - const result = await searchContent('pages', 'Searchable', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.matches.some((m) => m.path.includes('fields'))).toBe(true) - }) - - it('should find matches in arrays within objects', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Page', - page_type: 'landing_page', - published: '2023-01-01', - tags: ['keyword', 'searchable', 'content'], - }, - ]) - - const result = await searchContent('pages', 'searchable', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.matches.some((m) => m.path.includes('tags'))).toBe(true) - }) - - it('should handle deeply nested structures', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Page', - page_type: 'landing_page', - published: '2023-01-01', - data: { - level1: { - level2: { - level3: { - value: 'target keyword found', - }, - }, - }, - }, - }, - ]) - - const result = await searchContent('pages', 'target', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.matches.some((m) => m.path.includes('level3'))).toBe(true) - }) - - it('should find matches in multiple nested fields', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'search-keyword', - name: 'Search Title', - page_type: 'landing_page', - published: '2023-01-01', - meta: { - description: 'This contains search term', - keywords: ['search', 'seo'], - }, - }, - ]) - - const result = await searchContent('pages', 'search', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches.length).toBeGreaterThan(1) - }) - }) - - describe('Title and slug fallbacks', () => { - it('should use name as title for pages', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-slug', - name: 'Page Title', - page_type: 'landing_page', - published: '2023-01-01', - content: 'searchable content', - }, - ]) - - const result = await searchContent('pages', 'searchable', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results[0]!.title).toBe('Page Title') - }) - - it('should use title as fallback when name is missing', async () => { - mockGetAllPosts.mockResolvedValueOnce([ - { - id: '1', - slug: 'post-slug', - title: 'Post Title', - published: '2023-01-01', - content: 'searchable content', - }, - ]) - - const result = await searchContent('blog', 'searchable', 'test-token', false) - - expect(result.success).toBe(true) - expect(result.results[0]!.title).toBe('Post Title') - }) - - it('should use slug as fallback when name and title are missing', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'fallback-slug', - page_type: 'landing_page', - published: '2023-01-01', - content: 'searchable content here', - }, - ]) - - const result = await searchContent('pages', 'searchable', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.title).toBe('fallback-slug') - }) - - it('should use Untitled when no name, title, or slug available', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - page_type: 'landing_page', - published: '2023-01-01', - content: 'searchable content', - }, - ]) - - const result = await searchContent('pages', 'searchable', 'test-token', false, [ - 'landing_page', - ]) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.title).toBe('Untitled') - }) - - it('should use N/A for slug when slug is missing', async () => { - mockGetAllCollections.mockResolvedValueOnce([ - { - id: 1, - name: 'Collection Item', - }, - ]) - - const result = await searchContent( - 'collections', - 'Collection', - 'test-token', - false, - undefined, - ['items'], - ) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.slug).toBe('N/A') - }) - }) - - describe('Number and boolean matching', () => { - it('should match number values', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Page', - page_type: 'landing_page', - published: '2023-01-01', - priority: 5, - }, - ]) - - const result = await searchContent('pages', '5', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches.some((m) => m.value === '5')).toBe(true) - }) - - it('should match boolean values', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Page', - page_type: 'landing_page', - published: '2023-01-01', - featured: true, - }, - ]) - - const result = await searchContent('pages', 'true', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches.some((m) => m.value === 'true')).toBe(true) - }) - - it('should match partial numbers', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Page', - page_type: 'landing_page', - published: '2023-01-01', - year: 2023, - }, - ]) - - const result = await searchContent('pages', '202', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - }) - - describe('Context snippets', () => { - it('should include ellipsis before match when context precedes', async () => { - const longText = 'A'.repeat(150) + ' keyword ' + 'B'.repeat(50) - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: longText, - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches[0]!.value).toMatch(/^\.\.\./) - }) - - it('should include ellipsis after match when content follows', async () => { - const longText = 'keyword ' + 'B'.repeat(150) - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: longText, - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches[0]!.value).toMatch(/\.\.\.$/) - }) - - it('should not include ellipsis when match is at start', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'keyword is at the beginning', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches[0]!.value.startsWith('...')).toBe(false) - }) - - it('should not include ellipsis when match is at end', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'This ends with keyword', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches[0]!.value.endsWith('...')).toBe(false) - }) - - it('should create readable context around matches', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Before text with some context around the keyword and after text with more content', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - const snippet = result.results[0]!.matches[0]!.value - expect(snippet).toContain('keyword') - expect(snippet).toContain('context around') - expect(snippet).toContain('after text') - }) - }) - - describe('Special characters and edge cases', () => { - it('should handle special characters in search', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Price: $99.99 (special)', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', '$99.99', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - - it('should handle unicode characters', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Café résumé naïve', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'café', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - - it('should handle null and undefined values gracefully', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Valid Name', - page_type: 'landing_page', - published: '2023-01-01', - description: null, - metadata: undefined, - }, - ]) - - const result = await searchContent('pages', 'Valid', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - - it('should handle empty strings in fields', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Test Page', - page_type: 'landing_page', - published: '2023-01-01', - description: '', - content: 'findme', - }, - ]) - - const result = await searchContent('pages', 'findme', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - }) - }) - - describe('Empty and edge cases', () => { - it('should handle empty item array', async () => { - mockGetAllPages.mockResolvedValueOnce([]) - - const result = await searchContent('pages', 'anything', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(0) - expect(result.totalItems).toBe(0) - }) - - it('should handle items with no matching fields', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'Page One', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'xyz123', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(0) - }) - - it('should ignore whitespace-only matches in fields', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: ' ', - page_type: 'landing_page', - published: '2023-01-01', - description: 'Real content with keyword', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches.every((m) => m.value.trim().length > 0)).toBe(true) - }) - }) - - describe('Match count property', () => { - it('should include count property for single match', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'keyword found', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'keyword', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.matches[0]!.count).toBe(1) - }) - - it('should include accurate count for multiple matches', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'page-1', - name: 'match match match', - page_type: 'landing_page', - published: '2023-01-01', - }, - ]) - - const result = await searchContent('pages', 'match', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results[0]!.matches[0]!.count).toBe(3) - }) - }) - - describe('Integration tests', () => { - it('should handle complex real-world page object', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: 'page-123', - slug: 'product-launch', - name: 'Our New Product Launch', - page_type: 'landing_page', - published: '2023-06-15', - updated: '2023-06-20', - seo_title: 'Launch Your Business Success', - seo_description: 'Discover how our product can transform your business', - fields: { - hero_image: 'image.jpg', - hero_title: 'Launch Your Dreams', - hero_subtitle: 'Join thousands of successful businesses', - cta_text: 'Get Started Today', - cta_url: '/signup', - features: [ - { title: 'Fast Setup', description: 'Get launched in 5 minutes' }, - { title: 'Secure Platform', description: 'Enterprise-grade security' }, - ], - }, - tags: ['launch', 'product', 'business'], - author: { - name: 'John Doe', - email: 'john@example.com', - }, - }, - ]) - - const result = await searchContent('pages', 'launch', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(1) - expect(result.results[0]!.title).toBe('Our New Product Launch') - expect(result.results[0]!.matches.length).toBeGreaterThan(0) - }) - - it('should handle multiple pages with different match patterns', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'about', - name: 'About Company', - page_type: 'landing_page', - published: '2023-01-01', - content: 'We serve our customers', - }, - { - id: '2', - slug: 'blog', - name: 'Blog Page', - page_type: 'landing_page', - published: '2023-01-02', - content: 'Serve the community', - }, - { - id: '3', - slug: 'contact', - name: 'Contact Us', - page_type: 'landing_page', - published: '2023-01-03', - content: 'No match here', - }, - ]) - - const result = await searchContent('pages', 'serve', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(2) - expect(result.results[0]!.slug).toBe('about') - expect(result.results[1]!.slug).toBe('blog') - }) - - it('should handle partial word matching', async () => { - mockGetAllPages.mockResolvedValueOnce([ - { - id: '1', - slug: 'catalog', - name: 'Product Catalog', - page_type: 'landing_page', - published: '2023-01-01', - }, - { - id: '2', - slug: 'categories', - name: 'All Categories', - page_type: 'landing_page', - published: '2023-01-02', - }, - ]) - - const result = await searchContent('pages', 'cat', 'test-token', false, ['landing_page']) - - expect(result.success).toBe(true) - // Should match both "catalog" and "categories" - expect(result.results).toHaveLength(2) - }) - }) - - describe('Performance tests', () => { - it('should handle large datasets efficiently', async () => { - const largeDataset = Array.from({ length: 1000 }, (_, i) => ({ - id: String(i), - slug: `page-${i}`, - name: i % 10 === 0 ? `Found ${i}` : `Other ${i}`, - page_type: 'landing_page', - published: '2023-01-01', - })) - - mockGetAllPages.mockResolvedValueOnce(largeDataset) - - const startTime = Date.now() - const result = await searchContent('pages', 'Found', 'test-token', false, ['landing_page']) - const duration = Date.now() - startTime - - expect(result.success).toBe(true) - expect(result.results).toHaveLength(100) - expect(duration).toBeLessThan(2000) // Should complete in under 2 seconds - }) - - it('should handle objects with many fields', async () => { - const objectWithManyFields: Record = { - id: '1', - slug: 'complex', - name: 'Complex Object', - page_type: 'landing_page', - published: '2023-01-01', - } - - // Add 50 fields - for (let i = 0; i < 50; i++) { - objectWithManyFields[`field_${i}`] = i === 25 ? 'target keyword' : `value ${i}` - } - - mockGetAllPages.mockResolvedValueOnce([objectWithManyFields]) - - const result = await searchContent('pages', 'target', 'test-token', false, ['landing_page']) + const result = await searchContent('true', 'test-token', false, ['landing_page'], [], false) expect(result.success).toBe(true) expect(result.results).toHaveLength(1) diff --git a/src/features/searchContent.ts b/src/features/searchContent.ts index 2e20cfc..efb3933 100644 --- a/src/features/searchContent.ts +++ b/src/features/searchContent.ts @@ -128,12 +128,12 @@ function searchObject( } export async function searchContent( - scope: 'pages' | 'blog' | 'collections', searchString: string, token: string, preview: boolean, - pageTypes?: string[], - collectionKeys?: string[], + selectedPageTypes: string[], + selectedCollectionKeys: string[], + includeBlog: boolean, ): Promise { // Validate and normalize search input const trimmedSearch = searchString.trim() @@ -145,6 +145,16 @@ export async function searchContent( } } + // Validate that at least one search scope is selected + if (!includeBlog && selectedPageTypes.length === 0 && selectedCollectionKeys.length === 0) { + return { + success: false, + results: [], + totalItems: null, + error: 'Please select at least one search scope (Blog, Page Type, or Collection Key)', + } + } + const searchLower = trimmedSearch.toLowerCase() try { @@ -158,20 +168,27 @@ export async function searchContent( // Build items with source type tracking const itemsWithSource: Array<{ data: unknown; sourceType: string }> = [] - if (scope === 'pages' && pageTypes && pageTypes.length > 0) { - for (const pageType of pageTypes) { + // Search pages if any page types are selected + if (selectedPageTypes.length > 0) { + for (const pageType of selectedPageTypes) { const pages = await getAllPages({ token, pageType, preview }) pages.forEach((page) => { itemsWithSource.push({ data: page, sourceType: pageType }) }) } - } else if (scope === 'blog') { + } + + // Search blog posts if enabled + if (includeBlog) { const posts = await getAllPosts({ token, preview }) posts.forEach((post) => { itemsWithSource.push({ data: post, sourceType: 'Blog' }) }) - } else if (scope === 'collections' && collectionKeys && collectionKeys.length > 0) { - for (const collectionKey of collectionKeys) { + } + + // Search collections if any collection keys are selected + if (selectedCollectionKeys.length > 0) { + for (const collectionKey of selectedCollectionKeys) { const collections = await getAllCollections({ token, collectionType: collectionKey, @@ -181,8 +198,6 @@ export async function searchContent( itemsWithSource.push({ data: collection, sourceType: collectionKey }) }) } - } else { - throw new Error('Invalid search scope or missing required parameter') } for (const { data: itemData, sourceType } of itemsWithSource) { diff --git a/src/stores/index.ts b/src/stores/index.ts index f3c8bdb..786321e 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -3,7 +3,18 @@ import { defineStore } from 'pinia' export const useStore = defineStore('store', () => { // Initialize config from localStorage or default object - const config = ref( + const config = ref<{ + token: string + lockToken: boolean + includePreview: boolean + pageTypes: string[] + collectionKeys: string[] + selectedScopes: { + blog: boolean + pageTypes: string[] + collectionKeys: string[] + } + }>( (() => { const stored = localStorage.getItem('butter_cms_config') if (stored) { @@ -13,7 +24,18 @@ export const useStore = defineStore('store', () => { console.warn('Failed to parse stored config, using defaults') } } - return { token: '', lockToken: false, includePreview: false } + return { + token: '', + lockToken: false, + includePreview: false, + pageTypes: [], + collectionKeys: [], + selectedScopes: { + blog: false, + pageTypes: [], + collectionKeys: [], + }, + } })(), ) @@ -39,6 +61,27 @@ export const useStore = defineStore('store', () => { }, }) + const pageTypes = computed({ + get: () => config.value.pageTypes ?? [], + set: (val: string[]) => { + config.value.pageTypes = val + }, + }) + + const collectionKeys = computed({ + get: () => config.value.collectionKeys ?? [], + set: (val: string[]) => { + config.value.collectionKeys = val + }, + }) + + const selectedScopes = computed({ + get: () => config.value.selectedScopes ?? { blog: false, pageTypes: [], collectionKeys: [] }, + set: (val: { blog: boolean; pageTypes: string[]; collectionKeys: string[] }) => { + config.value.selectedScopes = val + }, + }) + // Watch for config changes and save to localStorage watch( config, @@ -48,5 +91,5 @@ export const useStore = defineStore('store', () => { { deep: true }, ) - return { token, lockToken, includePreview } + return { token, lockToken, includePreview, pageTypes, collectionKeys, selectedScopes } })