Add composable for managing feature flags

This commit is contained in:
2026-01-28 22:53:43 +00:00
parent bc48762925
commit 5151846a88

View File

@@ -0,0 +1,188 @@
/**
* Feature Flags Composable
*
* Manages runtime feature flag state with support for:
* - Local overrides (developer mode)
* - Permission-based flags (requires auth)
* - Backend feature flag queries (future)
*
* Usage:
* ```javascript
* const { isEnabled, toggle, getFlags } = useFeatureFlags();
* if (isEnabled('experimental-search')) { ... }
* ```
*/
import { ref, computed, readonly } from 'vue';
import { useAuth } from './useAuth.js';
import { FEATURE_FLAGS, getFlag, getFlagPermission } from '../config/feature-flags.js';
// Local storage key for overrides
const LOCAL_OVERRIDES_KEY = 'feature_flag_overrides';
/**
* Load flag overrides from localStorage
* @returns {Object} Overrides object
*/
function loadLocalOverrides() {
try {
const stored = localStorage.getItem(LOCAL_OVERRIDES_KEY);
return stored ? JSON.parse(stored) : {};
} catch {
return {};
}
}
/**
* Save flag overrides to localStorage
* @param {Object} overrides - Overrides to save
*/
function saveLocalOverrides(overrides) {
try {
localStorage.setItem(LOCAL_OVERRIDES_KEY, JSON.stringify(overrides));
} catch {
console.warn('Failed to save feature flag overrides');
}
}
// Shared state
const localOverrides = ref(loadLocalOverrides());
const backendFlags = ref({});
/**
* useFeatureFlags Composable
*
* @returns {Object} Feature flags interface
* - isEnabled(flagName) - Check if flag is enabled
* - toggle(flagName) - Toggle flag override in dev mode
* - reset(flagName) - Clear override for specific flag
* - resetAll() - Clear all overrides
* - getFlags() - Get all flags with status
* - setBackendFlags(flags) - Set flags from backend response
*/
export function useFeatureFlags() {
const { user, hasPermission } = useAuth();
/**
* Check if a feature flag is enabled
* Checks in order: local override, permission requirement, default value
*/
const isEnabled = computed(() => {
return (flagName) => {
// Check local override first
if (flagName in localOverrides.value) {
return localOverrides.value[flagName];
}
// Get flag definition
const flag = getFlag(flagName);
if (!flag) {
console.warn(`Feature flag not found: ${flagName}`);
return false;
}
// Check permission requirement
if (flag.requiredPermission && !hasPermission(flag.requiredPermission)) {
return false;
}
// Check backend flag (if available)
if (flagName in backendFlags.value) {
return backendFlags.value[flagName];
}
// Use default
return flag.enabled;
};
});
/**
* Toggle a flag override (dev mode only)
* Only works in development mode
*/
const toggle = (flagName) => {
if (process.env.NODE_ENV !== 'development') {
console.warn('Feature flag overrides only available in development mode');
return false;
}
const current = localOverrides.value[flagName] !== undefined
? localOverrides.value[flagName]
: getFlag(flagName)?.enabled ?? false;
localOverrides.value[flagName] = !current;
saveLocalOverrides(localOverrides.value);
return !current;
};
/**
* Reset a specific flag override
*/
const reset = (flagName) => {
if (process.env.NODE_ENV !== 'development') {
console.warn('Feature flag overrides only available in development mode');
return;
}
delete localOverrides.value[flagName];
saveLocalOverrides(localOverrides.value);
};
/**
* Reset all flag overrides
*/
const resetAll = () => {
if (process.env.NODE_ENV !== 'development') {
console.warn('Feature flag overrides only available in development mode');
return;
}
localOverrides.value = {};
saveLocalOverrides({});
};
/**
* Get all flags with their current status
*/
const getFlags = computed(() => {
return () => {
return Object.values(FEATURE_FLAGS).map(flag => ({
...flag,
isEnabled: isEnabled.value(flag.name),
hasOverride: flag.name in localOverrides.value,
override: localOverrides.value[flag.name],
requiresPermission: !!flag.requiredPermission,
hasPermission: !flag.requiredPermission || hasPermission(flag.requiredPermission)
}));
};
});
/**
* Set flags from backend response
* Called after fetching flags from backend
*/
const setBackendFlags = (flags) => {
backendFlags.value = flags;
};
/**
* Fetch flags from backend (requires admin permission)
* Future: Implement backend endpoint
*/
const fetchFromBackend = async () => {
// TODO: Implement backend endpoint
// const response = await apiClient.get('/api/feature-flags');
// setBackendFlags(response.flags);
};
return {
isEnabled,
toggle,
reset,
resetAll,
getFlags,
setBackendFlags,
fetchFromBackend
};
}