diff --git a/.eslintrc.js b/.eslintrc.js index 403a38e4c43..d982339d338 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,7 +52,7 @@ module.exports = { 'unused-imports', ], rules: { - "react-hooks/rules-of-hooks": "warn", + "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", // PyCharm always adds curly braces, I guess vscode doesn't, PR reviewers often complain they are present on props that don't need them // let's save the humans time and let the machines do the work diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png index 6018b88f109..74175953a8c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png index 907557cd8c2..a11674466fa 100644 Binary files a/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png and b/frontend/__snapshots__/scenes-other-onboarding--onboarding-billing--dark.png differ diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts b/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts index 7e9f9c8c8a5..bcce1caae3f 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.test.setup.ts @@ -41,6 +41,7 @@ async function testSetup( scope: ActivityScope, url: string ): Promise> { + // eslint-disable-next-line react-hooks/rules-of-hooks useMocks({ get: { [url]: { diff --git a/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx b/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx index d3c798462e7..139207f45e9 100644 --- a/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx +++ b/frontend/src/lib/components/Alerts/AlertDeletionWarning.tsx @@ -11,6 +11,7 @@ export function AlertDeletionWarning(): JSX.Element | null { return null } + // eslint-disable-next-line react-hooks/rules-of-hooks const { shouldShowAlertDeletionWarning } = useValues( alertsLogic({ insightShortId: insight.short_id, diff --git a/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx b/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx index f86aa17ebfd..f1d7410f53c 100644 --- a/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx +++ b/frontend/src/lib/components/DefinitionPopover/DefinitionPopoverContents.tsx @@ -479,13 +479,6 @@ export function ControlledDefinitionPopover({ group, highlightedItemElement, }: ControlledDefinitionPopoverContentsProps): JSX.Element | null { - // Supports all types specified in selectedItemHasPopover - const value = group.getValue?.(item) - - if (!value || !item) { - return null - } - const { state, singularType, definition } = useValues(definitionPopoverLogic) const { setDefinition } = useActions(definitionPopoverLogic) @@ -497,6 +490,13 @@ export function ControlledDefinitionPopover({ setDefinition(item) }, [item]) + // Supports all types specified in selectedItemHasPopover + const value = group.getValue?.(item) + + if (!value || !item) { + return null + } + return ( ({ let isPanelExpanded: (key: K) => boolean let onPanelChange: (key: K, isExpanded: boolean) => void if (props.multiple) { + // eslint-disable-next-line react-hooks/rules-of-hooks const [localActiveKeys, setLocalActiveKeys] = useState>(new Set(props.defaultActiveKeys ?? [])) const effectiveActiveKeys = props.activeKeys ? new Set(props.activeKeys) : localActiveKeys isPanelExpanded = (key: K) => effectiveActiveKeys.has(key) @@ -64,6 +65,7 @@ export function LemonCollapse({ setLocalActiveKeys(newActiveKeys) } } else { + // eslint-disable-next-line react-hooks/rules-of-hooks const [localActiveKey, setLocalActiveKey] = useState(props.defaultActiveKey ?? null) const effectiveActiveKey = props.activeKey ?? localActiveKey isPanelExpanded = (key: K) => key === effectiveActiveKey diff --git a/frontend/src/lib/lemon-ui/LemonToast/LemonToast.stories.tsx b/frontend/src/lib/lemon-ui/LemonToast/LemonToast.stories.tsx index 2fb39f85e51..d72455f44fa 100644 --- a/frontend/src/lib/lemon-ui/LemonToast/LemonToast.stories.tsx +++ b/frontend/src/lib/lemon-ui/LemonToast/LemonToast.stories.tsx @@ -46,6 +46,7 @@ export const ToastTypes: Story = { render: (args, { globals }) => { const isDarkModeOn = globals.theme === 'dark' + // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { lemonToast.dismiss() args.toasts.forEach((toast) => { diff --git a/frontend/src/scenes/billing/AllProductsPlanComparison.tsx b/frontend/src/scenes/billing/AllProductsPlanComparison.tsx index d3bfb227b91..ede32d9082d 100644 --- a/frontend/src/scenes/billing/AllProductsPlanComparison.tsx +++ b/frontend/src/scenes/billing/AllProductsPlanComparison.tsx @@ -134,16 +134,9 @@ export const AllProductsPlanComparison = ({ product: BillingProductV2Type includeAddons?: boolean }): JSX.Element | null => { - const plans = product.plans?.filter( - (plan) => !plan.included_if || plan.included_if == 'has_subscription' || plan.current_plan - ) - if (plans?.length === 0) { - return null - } const { billing, redirectPath, timeRemainingInSeconds, timeTotalInSeconds } = useValues(billingLogic) const { ref: planComparisonRef } = useResizeObserver() const { reportBillingUpgradeClicked, reportBillingDowngradeClicked } = useActions(eventUsageLogic) - const currentPlanIndex = plans.findIndex((plan) => plan.current_plan) const { surveyID, comparisonModalHighlightedFeatureKey, billingProductLoading } = useValues( billingProductLogic({ product }) ) @@ -152,6 +145,14 @@ export const AllProductsPlanComparison = ({ ) const { featureFlags } = useValues(featureFlagLogic) + const plans = product.plans?.filter( + (plan) => !plan.included_if || plan.included_if == 'has_subscription' || plan.current_plan + ) + if (plans?.length === 0) { + return null + } + const currentPlanIndex = plans.findIndex((plan) => plan.current_plan) + const nonInclusionProducts = billing?.products.filter((p) => !p.inclusion_only) || [] const inclusionProducts = billing?.products.filter((p) => !!p.inclusion_only) || [] const sortedProducts = nonInclusionProducts diff --git a/frontend/src/scenes/billing/PlanComparison.tsx b/frontend/src/scenes/billing/PlanComparison.tsx index 47af94b1434..00b5cc132ee 100644 --- a/frontend/src/scenes/billing/PlanComparison.tsx +++ b/frontend/src/scenes/billing/PlanComparison.tsx @@ -112,17 +112,9 @@ export const PlanComparison = ({ product: BillingProductV2Type includeAddons?: boolean }): JSX.Element | null => { - const plans = product.plans?.filter( - (plan) => !plan.included_if || plan.included_if == 'has_subscription' || plan.current_plan - ) - if (plans?.length === 0) { - return null - } - const fullyFeaturedPlan = plans[plans.length - 1] const { billing, redirectPath, timeRemainingInSeconds, timeTotalInSeconds } = useValues(billingLogic) const { width, ref: planComparisonRef } = useResizeObserver() const { reportBillingUpgradeClicked, reportBillingDowngradeClicked } = useActions(eventUsageLogic) - const currentPlanIndex = plans.findIndex((plan) => plan.current_plan) const { surveyID, comparisonModalHighlightedFeatureKey, billingProductLoading } = useValues( billingProductLogic({ product }) ) @@ -131,6 +123,15 @@ export const PlanComparison = ({ ) const { featureFlags } = useValues(featureFlagLogic) + const plans = product.plans?.filter( + (plan) => !plan.included_if || plan.included_if == 'has_subscription' || plan.current_plan + ) + if (plans?.length === 0) { + return null + } + const currentPlanIndex = plans.findIndex((plan) => plan.current_plan) + const fullyFeaturedPlan = plans[plans.length - 1] + const ctaAction = billing?.subscription_level === 'custom' ? 'Subscribe' : 'Upgrade' const upgradeButtons = plans?.map((plan, i) => { return ( diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx index 5d3bc286050..737724ae7eb 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx @@ -620,6 +620,7 @@ function UsageTab({ featureFlag }: { id: string; featureFlag: FeatureFlagType }) let dashboard: DashboardType | null = null if (dashboardId) { // FIXME: Refactor out into , as React hooks under conditional branches are no good + // eslint-disable-next-line react-hooks/rules-of-hooks const dashboardLogicValues = useValues( dashboardLogic({ id: dashboardId, placement: DashboardPlacement.FeatureFlag }) ) diff --git a/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx b/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx index 99ce22a5453..488bd19ab36 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagProjects.tsx @@ -11,15 +11,14 @@ import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' import { cohortsModel } from '~/models/cohortsModel' -import { groupsModel } from '~/models/groupsModel' -import { FeatureFlagType, OrganizationFeatureFlag } from '~/types' +import { groupsModel, type Noun } from '~/models/groupsModel' +import { CohortType, FeatureFlagType, OrganizationFeatureFlag, OrganizationType } from '~/types' import { organizationLogic } from '../organizationLogic' import { featureFlagLogic } from './featureFlagLogic' import { groupFilters } from './FeatureFlags' -function checkHasStaticCohort(featureFlag: FeatureFlagType): boolean { - const { cohorts } = useValues(cohortsModel) +function checkHasStaticCohort(featureFlag: FeatureFlagType, cohorts: CohortType[]): boolean { const staticCohorts = new Set() cohorts.forEach((cohort) => { if (cohort.is_static) { @@ -37,11 +36,15 @@ function checkHasStaticCohort(featureFlag: FeatureFlagType): boolean { return false } -const getColumns = (): LemonTableColumns => { - const { currentTeamId } = useValues(teamLogic) - const { currentOrganization } = useValues(organizationLogic) - const { aggregationLabel } = useValues(groupsModel) - +const getColumns = ({ + aggregationLabel, + currentTeamId, + currentOrganization, +}: { + aggregationLabel: (groupTypeIndex: number | null | undefined, deferToUserWording?: boolean) => Noun + currentTeamId: number | null + currentOrganization: OrganizationType | null +}): LemonTableColumns => { return [ { title: 'Project', @@ -133,8 +136,9 @@ function FeatureFlagCopySection(): JSX.Element { const { setCopyDestinationProject, copyFlag } = useActions(featureFlagLogic) const { currentOrganization } = useValues(organizationLogic) const { currentTeam } = useValues(teamLogic) + const { cohorts } = useValues(cohortsModel) - const hasStaticCohort = checkHasStaticCohort(featureFlag) + const hasStaticCohort = checkHasStaticCohort(featureFlag, cohorts) const hasMultipleProjects = (currentOrganization?.teams?.length ?? 0) > 1 return hasMultipleProjects && featureFlag.can_edit ? ( @@ -198,6 +202,9 @@ function FeatureFlagCopySection(): JSX.Element { export default function FeatureFlagProjects(): JSX.Element { const { projectsWithCurrentFlag } = useValues(featureFlagLogic) const { loadProjectsWithCurrentFlag } = useActions(featureFlagLogic) + const { currentTeamId } = useValues(teamLogic) + const { currentOrganization } = useValues(organizationLogic) + const { aggregationLabel } = useValues(groupsModel) useEffect(() => { loadProjectsWithCurrentFlag() @@ -210,7 +217,7 @@ export default function FeatureFlagProjects(): JSX.Element { diff --git a/frontend/src/scenes/insights/filters/AggregationSelect.tsx b/frontend/src/scenes/insights/filters/AggregationSelect.tsx index 6ddb5cca811..1aae9ea390e 100644 --- a/frontend/src/scenes/insights/filters/AggregationSelect.tsx +++ b/frontend/src/scenes/insights/filters/AggregationSelect.tsx @@ -45,6 +45,9 @@ export function AggregationSelect({ const { querySource } = useValues(insightVizDataLogic(insightProps)) const { updateQuerySource } = useActions(insightVizDataLogic(insightProps)) + const { groupTypes, aggregationLabel } = useValues(groupsModel) + const { needsUpgradeForGroups, canStartUsingGroups } = useValues(groupsAccessLogic) + if (!isInsightQueryNode(querySource)) { return null } @@ -64,8 +67,6 @@ export function AggregationSelect({ updateQuerySource({ aggregation_group_type_index: groupIndex } as FunnelsQuery) } } - const { groupTypes, aggregationLabel } = useValues(groupsModel) - const { needsUpgradeForGroups, canStartUsingGroups } = useValues(groupsAccessLogic) const baseValues = [UNIQUE_USERS] const optionSections: LemonSelectSection[] = [ diff --git a/frontend/src/scenes/insights/utils/cleanFilters.ts b/frontend/src/scenes/insights/utils/cleanFilters.ts index 2e3c2022ff8..b0ba0308bec 100644 --- a/frontend/src/scenes/insights/utils/cleanFilters.ts +++ b/frontend/src/scenes/insights/utils/cleanFilters.ts @@ -145,6 +145,8 @@ const cleanBreakdownParams = (cleanedParams: Partial, filters: Parti // For the map, make sure we are breaking down by country // Support automatic switching to country code breakdown both from no breakdown and from country name breakdown cleanedParams['breakdown'] = '$geoip_country_code' + // this isn't a react hook + // eslint-disable-next-line react-hooks/rules-of-hooks useMostRelevantBreakdownType(cleanedParams, filters) return } diff --git a/frontend/src/scenes/persons/PersonDisplay.tsx b/frontend/src/scenes/persons/PersonDisplay.tsx index 15555552957..6f8067eb455 100644 --- a/frontend/src/scenes/persons/PersonDisplay.tsx +++ b/frontend/src/scenes/persons/PersonDisplay.tsx @@ -112,10 +112,12 @@ export function PersonDisplay({ ) : ( setVisible(false)} - /> + person?.distinct_id || person?.distinct_ids?.[0] ? ( + setVisible(false)} + /> + ) : null } visible={visible} onClickOutside={() => setVisible(false)} diff --git a/frontend/src/scenes/persons/PersonPreview.tsx b/frontend/src/scenes/persons/PersonPreview.tsx index ce8a554280c..0e5d121b300 100644 --- a/frontend/src/scenes/persons/PersonPreview.tsx +++ b/frontend/src/scenes/persons/PersonPreview.tsx @@ -21,7 +21,7 @@ export function PersonPreview(props: PersonPreviewProps): JSX.Element | null { if (!props.distinctId) { return null } - + // eslint-disable-next-line react-hooks/rules-of-hooks const { person, personLoading } = useValues(personLogic({ id: props.distinctId })) if (personLoading) { diff --git a/frontend/src/scenes/pipeline/utils.tsx b/frontend/src/scenes/pipeline/utils.tsx index 25fb1961626..1b3171ec9a0 100644 --- a/frontend/src/scenes/pipeline/utils.tsx +++ b/frontend/src/scenes/pipeline/utils.tsx @@ -262,6 +262,7 @@ function pluginMenuItems(node: PluginBasedNode): LemonMenuItem[] { } export function pipelineNodeMenuCommonItems(node: Transformation | SiteApp | ImportApp | Destination): LemonMenuItem[] { + // eslint-disable-next-line react-hooks/rules-of-hooks const { canConfigurePlugins } = useValues(pipelineAccessLogic) const items: LemonMenuItem[] = [ @@ -295,6 +296,7 @@ export function pipelinePluginBackedNodeMenuCommonItems( loadPluginConfigs: any, inOverview?: boolean ): LemonMenuItem[] { + // eslint-disable-next-line react-hooks/rules-of-hooks const { canConfigurePlugins } = useValues(pipelineAccessLogic) return [ diff --git a/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx b/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx index 675268c115f..dae06c8ba2e 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerMetaLinks.tsx @@ -176,7 +176,8 @@ const MenuActions = (): JSX.Element => { useActions(sessionRecordingPlayerLogic) const { fetchSimilarRecordings } = useActions(sessionRecordingDataLogic(logicProps)) - const hasMobileExport = window.IMPERSONATED_SESSION || useFeatureFlag('SESSION_REPLAY_EXPORT_MOBILE_DATA') + const hasMobileExportFlag = useFeatureFlag('SESSION_REPLAY_EXPORT_MOBILE_DATA') + const hasMobileExport = window.IMPERSONATED_SESSION || hasMobileExportFlag const hasSimilarRecordings = useFeatureFlag('REPLAY_SIMILAR_RECORDINGS') const onDelete = (): void => { diff --git a/frontend/src/scenes/settings/organization/Members.tsx b/frontend/src/scenes/settings/organization/Members.tsx index 5f6aab35113..cceeaa73e53 100644 --- a/frontend/src/scenes/settings/organization/Members.tsx +++ b/frontend/src/scenes/settings/organization/Members.tsx @@ -145,14 +145,14 @@ export function Members(): JSX.Element | null { const { preflight } = useValues(preflightLogic) const { user } = useValues(userLogic) - if (!user) { - return null - } - useEffect(() => { ensureAllMembersLoaded() }, []) + if (!user) { + return null + } + const columns: LemonTableColumns = [ { key: 'user_profile_picture', diff --git a/frontend/src/toolbar/bar/Toolbar.tsx b/frontend/src/toolbar/bar/Toolbar.tsx index 6a44a1711d9..8c670fa3f3e 100644 --- a/frontend/src/toolbar/bar/Toolbar.tsx +++ b/frontend/src/toolbar/bar/Toolbar.tsx @@ -95,7 +95,8 @@ export function ToolbarInfoMenu(): JSX.Element | null { const { visibleMenu, isDragging, menuProperties, minimized, isBlurred } = useValues(toolbarLogic) const { setMenu } = useActions(toolbarLogic) const { isAuthenticated } = useValues(toolbarConfigLogic) - const showExperiments = inStorybook() || inStorybookTestRunner() ? true : useToolbarFeatureFlag('web-experiments') + const showExperimentsFlag = useToolbarFeatureFlag('web-experiments') + const showExperiments = inStorybook() || inStorybookTestRunner() ? true : showExperimentsFlag const content = minimized ? null : visibleMenu === 'flags' ? ( ) : visibleMenu === 'heatmap' ? ( @@ -153,7 +154,8 @@ export function Toolbar(): JSX.Element | null { const { setVisibleMenu, toggleMinimized, onMouseOrTouchDown, setElement, setIsBlurred } = useActions(toolbarLogic) const { isAuthenticated, userIntent } = useValues(toolbarConfigLogic) const { authenticate } = useActions(toolbarConfigLogic) - const showExperiments = inStorybook() || inStorybookTestRunner() ? true : useToolbarFeatureFlag('web-experiments') + const showExperimentsFlag = useToolbarFeatureFlag('web-experiments') + const showExperiments = inStorybook() || inStorybookTestRunner() ? true : showExperimentsFlag useEffect(() => { setElement(ref.current)