* feat: add scope configuration for feature opt-in Add scope field to OptInFeatureConfig that allows features to be scoped to specific levels (org, team, user). This enables features to be shown only at certain settings pages rather than all three. Changes: - Add OptInFeatureScope type with values 'org', 'team', 'user' - Add optional scope field to OptInFeatureConfig interface - Add getOptInFeaturesForScope helper function to filter features by scope - Update FeatureOptInService to filter features based on scope - Update tRPC router to pass scope parameter for org/team endpoints Features without a scope field default to all scopes for backward compatibility. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * feat: add scope validation to setUserFeatureState and setTeamFeatureState - Add isFeatureAllowedForScope helper function to check if a feature is allowed for a scope - Update setUserFeatureState to reject if feature is not scoped to 'user' - Update setTeamFeatureState to accept scope parameter and reject if feature is not allowed - Update tRPC router to pass scope parameter for team and org endpoints - Fix unit test mock to include new config exports Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * feat: use ErrorWithCode for scope validation and add tests - Replace raw Error with ErrorWithCode using ErrorCode.BadRequest - Add comprehensive tests for setUserFeatureState scope validation - Add comprehensive tests for setTeamFeatureState scope validation - Test both enabled/disabled and inherit state scenarios - Test error messages include feature ID and scope name Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * feat: improve Features menu visibility to use scope configuration Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: remove unused hasOptInFeaturesForScope function Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: allow features not in config at all scopes (permissive default) Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: revert permissive default and mock scope validation in integration tests Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import type { FeatureId } from "@calcom/features/flags/config";
|
|
import type { OptInFeaturePolicy, OptInFeatureScope } from "./types";
|
|
|
|
export interface OptInFeatureConfig {
|
|
slug: FeatureId;
|
|
titleI18nKey: string;
|
|
descriptionI18nKey: string;
|
|
policy: OptInFeaturePolicy;
|
|
/** Scopes where this feature can be configured. Defaults to all scopes if not specified. */
|
|
scope?: OptInFeatureScope[];
|
|
}
|
|
|
|
/** All available scopes for feature opt-in configuration */
|
|
export const ALL_SCOPES: OptInFeatureScope[] = ["org", "team", "user"];
|
|
|
|
/**
|
|
* Features that appear in opt-in settings.
|
|
* Add new features here to make them available for user/team opt-in.
|
|
*/
|
|
export const OPT_IN_FEATURES: OptInFeatureConfig[] = [
|
|
// Example - to be populated with actual features
|
|
// {
|
|
// slug: "bookings-v3",
|
|
// titleI18nKey: "bookings_v3_title",
|
|
// descriptionI18nKey: "bookings_v3_description",
|
|
// policy: "permissive",
|
|
// scope: ["org", "team", "user"], // Optional: defaults to all scopes if not specified
|
|
// },
|
|
];
|
|
|
|
/**
|
|
* Get the configuration for a specific opt-in feature by slug.
|
|
*/
|
|
export function getOptInFeatureConfig(slug: string): OptInFeatureConfig | undefined {
|
|
return OPT_IN_FEATURES.find((f) => f.slug === slug);
|
|
}
|
|
|
|
/**
|
|
* Check if a slug is in the opt-in allowlist.
|
|
* Acts as a type guard, narrowing the slug to FeatureId when true.
|
|
*/
|
|
export function isOptInFeature(slug: string): slug is FeatureId {
|
|
return OPT_IN_FEATURES.some((f) => f.slug === slug);
|
|
}
|
|
|
|
/**
|
|
* Check if there are any opt-in features available.
|
|
*/
|
|
export const HAS_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.length > 0;
|
|
|
|
/** Whether there are opt-in features available for the user scope */
|
|
export const HAS_USER_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("user"));
|
|
|
|
/** Whether there are opt-in features available for the team scope */
|
|
export const HAS_TEAM_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("team"));
|
|
|
|
/** Whether there are opt-in features available for the org scope */
|
|
export const HAS_ORG_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("org"));
|
|
|
|
/**
|
|
* Get opt-in features that are available for a specific scope.
|
|
* Features without a scope field are available for all scopes.
|
|
*/
|
|
export function getOptInFeaturesForScope(scope: OptInFeatureScope): OptInFeatureConfig[] {
|
|
return OPT_IN_FEATURES.filter((f) => !f.scope || f.scope.includes(scope));
|
|
}
|
|
|
|
/**
|
|
* Check if a feature is allowed for a specific scope.
|
|
* Features without a scope field are allowed for all scopes.
|
|
* Features not in the config are NOT allowed (must be explicitly configured).
|
|
*/
|
|
export function isFeatureAllowedForScope(slug: string, scope: OptInFeatureScope): boolean {
|
|
const config = getOptInFeatureConfig(slug);
|
|
if (!config) return false;
|
|
return !config.scope || config.scope.includes(scope);
|
|
}
|