✨ Add composable for managing feature flags
This commit is contained in:
188
code/websites/pokedex.online/src/composables/useFeatureFlags.js
Normal file
188
code/websites/pokedex.online/src/composables/useFeatureFlags.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user