diff --git a/frontend/src/views/settings/SystemSettings.vue b/frontend/src/views/settings/SystemSettings.vue index 57c7194..ff18a5f 100644 --- a/frontend/src/views/settings/SystemSettings.vue +++ b/frontend/src/views/settings/SystemSettings.vue @@ -414,6 +414,32 @@ + + +
+

Global Search

+ +
+

+ Choose which content types appear in global search results. Disabling a + type here hides it from search without disabling the plugin elsewhere. +

+ +
+ +
+
+
{{ error }}
@@ -476,6 +502,26 @@ function matrixValue(name, assettype) { return key in identifierMatrix ? identifierMatrix[key] : true } +// Global-search domain toggles: keys follow search__enabled. +// Missing = enabled (default on). +const searchDomains = [ + { key: 'application', label: 'Applications' }, + { key: 'knowledgebase', label: 'Knowledge Base' }, + { key: 'employee', label: 'Employees' }, + { key: 'equipment', label: 'Equipment' }, + { key: 'computer', label: 'PCs' }, + { key: 'printer', label: 'Printers' }, + { key: 'network_device', label: 'Network Devices' }, + { key: 'notification', label: 'Notifications' }, + { key: 'subnet', label: 'Subnets' } +] +const searchMatrix = reactive({}) + +function searchValue(domainKey) { + const key = `search_${domainKey}_enabled` + return key in searchMatrix ? searchMatrix[key] : true +} + const loading = ref(true) const saving = ref(false) const testingEmail = ref(false) @@ -538,6 +584,8 @@ async function loadSettings() { settings[setting.key] = setting.value } else if (/^identifier_.+_(equipment|computer|printer|network_device)_enabled$/.test(setting.key)) { identifierMatrix[setting.key] = setting.value !== false + } else if (/^search_.+_enabled$/.test(setting.key)) { + searchMatrix[setting.key] = setting.value !== false } } } catch (e) { @@ -591,6 +639,40 @@ async function toggleIdentifier(name, assettype) { } } +async function toggleSearchDomain(domainKey) { + const key = `search_${domainKey}_enabled` + const newValue = !searchValue(domainKey) + const label = searchDomains.find(d => d.key === domainKey)?.label || domainKey + try { + saving.value = true + error.value = '' + success.value = '' + try { + await settingsApi.update(key, newValue) + } catch (e) { + if (e.response?.status === 404) { + await settingsApi.create({ + key, + value: newValue, + valuetype: 'boolean', + category: 'search', + description: `Include ${label} in global search results` + }) + } else { + throw e + } + } + searchMatrix[key] = newValue + success.value = 'Setting saved' + setTimeout(() => { success.value = '' }, 2000) + } catch (e) { + error.value = e.response?.data?.message || 'Failed to save setting' + console.error(e) + } finally { + saving.value = false + } +} + async function saveSetting(key, value) { try { saving.value = true diff --git a/shopdb/core/api/search.py b/shopdb/core/api/search.py index 62e2097..0bab4db 100644 --- a/shopdb/core/api/search.py +++ b/shopdb/core/api/search.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import joinedload from shopdb.extensions import db from shopdb.core.models import ( - Application, + Application, Setting, Asset, AssetType, Communication, Vendor, Model ) from shopdb.utils.responses import success_response @@ -718,6 +718,16 @@ def global_search(): seen_ids[key] = True unique_results.append(r) + # Drop result types disabled in Settings > Search (search__enabled). + # Missing key = enabled. One query, default-on. + disabled_types = { + s.key[len('search_'):-len('_enabled')] + for s in Setting.query.filter_by(category='search').all() + if s.get_typed_value() is False + } + if disabled_types: + unique_results = [r for r in unique_results if r['type'] not in disabled_types] + # Compute type counts before truncation type_counts = {} for r in unique_results: diff --git a/shopdb/core/api/settings.py b/shopdb/core/api/settings.py index 5533f66..dff9a1c 100644 --- a/shopdb/core/api/settings.py +++ b/shopdb/core/api/settings.py @@ -27,6 +27,22 @@ IDENTIFIER_LABELS = { } IDENTIFIER_ASSETTYPES = ['equipment', 'computer', 'printer', 'network_device'] +# Global-search result types that can be toggled on/off independently of whether +# the owning plugin is enabled. Keys match the `type` field on search results; +# seed keys are search__enabled (boolean, default true). Drives the +# Settings "Search" toggles and the filter in shopdb/core/api/search.py. +SEARCH_DOMAINS = { + 'application': 'Applications', + 'knowledgebase': 'Knowledge Base', + 'employee': 'Employees', + 'equipment': 'Equipment', + 'computer': 'PCs', + 'printer': 'Printers', + 'network_device': 'Network Devices', + 'notification': 'Notifications', + 'subnet': 'Subnets', +} + def _is_secret(key: str) -> bool: return 'password' in key or 'token' in key or 'secret' in key @@ -182,7 +198,19 @@ def build_default_settings(): for assettype in IDENTIFIER_ASSETTYPES ] - defaults = identifierdefaults + [ + # Per-domain global-search toggles (search__enabled). + searchdefaults = [ + { + 'key': f'search_{key}_enabled', + 'value': 'true', + 'valuetype': 'boolean', + 'category': 'search', + 'description': f'Include {label} in global search results', + } + for key, label in SEARCH_DOMAINS.items() + ] + + defaults = identifierdefaults + searchdefaults + [ # Zabbix integration { 'key': 'zabbix_enabled', diff --git a/tests/test_core/test_search_settings.py b/tests/test_core/test_search_settings.py new file mode 100644 index 0000000..2514ab2 --- /dev/null +++ b/tests/test_core/test_search_settings.py @@ -0,0 +1,44 @@ +"""Per-domain global-search toggles (search__enabled). + +A content type can be hidden from search independently of whether its plugin is +enabled, by setting search__enabled = false. +""" + +from shopdb.core.api.settings import SEARCH_DOMAINS + + +def _kb_types(client, auth_headers, term): + resp = client.get(f'/api/search?q={term}', headers=auth_headers) + assert resp.status_code == 200, resp.get_json() + return [r for r in resp.get_json()['data']['results'] + if r.get('type') == 'knowledgebase'] + + +def test_domain_included_by_default(client, db, auth_headers): + """With no toggle set, the knowledgebase domain appears in search.""" + client.post('/api/knowledgebase', + json={'shortdescription': 'searchtoggle alpha', + 'linkurl': 'https://kb.example/a'}, + headers=auth_headers) + assert _kb_types(client, auth_headers, 'searchtoggle') + + +def test_disabled_domain_excluded(client, db, auth_headers): + """search_knowledgebase_enabled=false hides KB results.""" + from shopdb.core.models import Setting + client.post('/api/knowledgebase', + json={'shortdescription': 'searchtoggle beta', + 'linkurl': 'https://kb.example/b'}, + headers=auth_headers) + Setting.set('search_knowledgebase_enabled', False, valuetype='boolean', + category='search') + assert _kb_types(client, auth_headers, 'searchtoggle') == [] + + +def test_search_seed_creates_domain_keys(client, db, auth_headers): + """Seeding produces one boolean setting per search domain.""" + resp = client.post('/api/settings/seed', headers=auth_headers) + assert resp.status_code == 200 + from shopdb.core.models import Setting + keys = {s.key for s in Setting.query.filter_by(category='search').all()} + assert {f'search_{k}_enabled' for k in SEARCH_DOMAINS} <= keys