diff --git a/frontend/src/layout/navigation-3000/components/TopBar.tsx b/frontend/src/layout/navigation-3000/components/TopBar.tsx index 5a8bc2f2a235c..af640b56fcf41 100644 --- a/frontend/src/layout/navigation-3000/components/TopBar.tsx +++ b/frontend/src/layout/navigation-3000/components/TopBar.tsx @@ -237,6 +237,7 @@ function Here({ breadcrumb }: HereProps): JSX.Element { placeholder="Unnamed" compactButtons="xsmall" editingIndication="underlined" + autoFocus /> ) : ( {breadcrumb.name} diff --git a/frontend/src/lib/components/EditableField/EditableField.tsx b/frontend/src/lib/components/EditableField/EditableField.tsx index 3b25fd7a4be8e..e8cc91c67ef5e 100644 --- a/frontend/src/lib/components/EditableField/EditableField.tsx +++ b/frontend/src/lib/components/EditableField/EditableField.tsx @@ -1,12 +1,13 @@ import './EditableField.scss' +import { useMergeRefs } from '@floating-ui/react' import clsx from 'clsx' import { IconEdit, IconMarkdown } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonMarkdown } from 'lib/lemon-ui/LemonMarkdown' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { pluralize } from 'lib/utils' -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' import TextareaAutosize from 'react-textarea-autosize' export interface EditableFieldProps { @@ -51,7 +52,7 @@ export function EditableField({ placeholder, minLength, maxLength, - autoFocus = true, + autoFocus = false, multiline = false, markdown = false, compactButtons = false, @@ -65,16 +66,30 @@ export function EditableField({ saveButtonText = 'Save', notice, }: EditableFieldProps): JSX.Element { - const [localIsEditing, setLocalIsEditing] = useState(false) + const [localIsEditing, setLocalIsEditing] = useState(mode === 'edit') const [localTentativeValue, setLocalTentativeValue] = useState(value) + const inputRef = useRef() + const previousIsEditing = useRef() useEffect(() => { setLocalTentativeValue(value) }, [value]) + useEffect(() => { setLocalIsEditing(mode === 'edit') }, [mode]) + useEffect(() => { + // We always want to focus when switching to edit mode, but can't use autoFocus, because we don't want this to + // happen when the component is _initially_ rendered in edit mode. + if (inputRef.current && previousIsEditing.current === false && localIsEditing === true) { + const endOfInput = inputRef.current.value.length + inputRef.current.setSelectionRange(endOfInput, endOfInput) + inputRef.current.focus() + } + previousIsEditing.current = localIsEditing + }, [localIsEditing]) + const isSaveable = !minLength || localTentativeValue.length >= minLength const mouseDownOnCancelButton = (e: React.MouseEvent): void => { @@ -150,6 +165,7 @@ export function EditableField({ minLength={minLength} maxLength={maxLength} autoFocus={autoFocus} + ref={inputRef as React.RefObject} /> ) : ( } /> )} {(!mode || !!onModeToggle) && ( @@ -242,17 +259,7 @@ export function EditableField({ ) } -const AutosizeInput = ({ - name, - value, - onChange, - placeholder, - onBlur, - onKeyDown, - minLength, - maxLength, - autoFocus, -}: { +interface AutosizeInputProps { name: string value: string onChange: (e: React.ChangeEvent) => void @@ -262,11 +269,18 @@ const AutosizeInput = ({ minLength?: number maxLength?: number autoFocus?: boolean -}): JSX.Element => { +} + +const AutosizeInput = React.forwardRef(function AutosizeInput( + { name, value, onChange, placeholder, onBlur, onKeyDown, minLength, maxLength, autoFocus }, + ref +) { const [inputWidth, setInputWidth] = useState(1) - const inputRef = useRef(null) + const [inputStyles, setInputStyles] = useState() const sizerRef = useRef(null) const placeHolderSizerRef = useRef(null) + const inputRef = useRef(null) + const mergedRefs = useMergeRefs([ref, inputRef]) const copyStyles = (styles: CSSStyleDeclaration, node: HTMLDivElement): void => { node.style.fontSize = styles.fontSize @@ -277,21 +291,22 @@ const AutosizeInput = ({ node.style.textTransform = styles.textTransform } - const inputStyles = useMemo(() => { - return inputRef.current ? window.getComputedStyle(inputRef.current) : null - }, [inputRef.current]) - useLayoutEffect(() => { - if (inputStyles && placeHolderSizerRef.current) { - copyStyles(inputStyles, placeHolderSizerRef.current) + if (inputRef.current) { + setInputStyles(getComputedStyle(inputRef.current)) } - }, [placeHolderSizerRef, placeHolderSizerRef]) + }, [inputRef.current]) useLayoutEffect(() => { - if (inputStyles && sizerRef.current) { - copyStyles(inputStyles, sizerRef.current) + if (inputStyles) { + if (sizerRef.current) { + copyStyles(inputStyles, sizerRef.current) + } + if (placeHolderSizerRef.current) { + copyStyles(inputStyles, placeHolderSizerRef.current) + } } - }, [inputStyles, sizerRef]) + }, [inputStyles]) useLayoutEffect(() => { if (!sizerRef.current || !placeHolderSizerRef.current) { @@ -320,7 +335,7 @@ const AutosizeInput = ({ minLength={minLength} maxLength={maxLength} autoFocus={autoFocus} - ref={inputRef} + ref={mergedRefs} /* eslint-disable-next-line react/forbid-dom-props */ style={{ boxSizing: 'content-box', width: `${inputWidth}px` }} /> @@ -332,4 +347,4 @@ const AutosizeInput = ({ ) -} +}) diff --git a/frontend/src/scenes/actions/actionEditLogic.tsx b/frontend/src/scenes/actions/actionEditLogic.tsx index 5680eea4ae1c8..5d3c8d193285f 100644 --- a/frontend/src/scenes/actions/actionEditLogic.tsx +++ b/frontend/src/scenes/actions/actionEditLogic.tsx @@ -69,9 +69,6 @@ export const actionEditLogic = kea([ forms(({ actions, props }) => ({ action: { defaults: { ...props.action } as ActionEditType, - errors: ({ name }) => ({ - name: !name ? 'You need to set a name' : null, - }), submit: (action) => { actions.saveAction(action) }, diff --git a/frontend/src/scenes/data-management/actions/ActionsTable.tsx b/frontend/src/scenes/data-management/actions/ActionsTable.tsx index 8efad967b31b3..997c7f0378c1c 100644 --- a/frontend/src/scenes/data-management/actions/ActionsTable.tsx +++ b/frontend/src/scenes/data-management/actions/ActionsTable.tsx @@ -47,7 +47,7 @@ export function ActionsTable(): JSX.Element { return ( <> - {name || Unnamed action} + {name || Unnamed} {action.description && ( diff --git a/frontend/src/scenes/insights/insightSceneLogic.tsx b/frontend/src/scenes/insights/insightSceneLogic.tsx index 8a8f1fda9cd0e..eb55a25fa0aa0 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.tsx +++ b/frontend/src/scenes/insights/insightSceneLogic.tsx @@ -9,13 +9,17 @@ import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' import { teamLogic } from 'scenes/teamLogic' +import { mathsLogic } from 'scenes/trends/mathsLogic' import { urls } from 'scenes/urls' +import { cohortsModel } from '~/models/cohortsModel' +import { groupsModel } from '~/models/groupsModel' import { Breadcrumb, FilterType, InsightShortId, InsightType, ItemMode } from '~/types' import { insightDataLogic } from './insightDataLogic' import { insightDataLogicType } from './insightDataLogicType' import type { insightSceneLogicType } from './insightSceneLogicType' +import { summarizeInsight } from './summarizeInsight' export const insightSceneLogic = kea([ path(['scenes', 'insights', 'insightSceneLogic']), @@ -85,10 +89,19 @@ export const insightSceneLogic = kea([ }), selectors(() => ({ insightSelector: [(s) => [s.insightLogicRef], (insightLogicRef) => insightLogicRef?.logic.selectors.insight], + filtersSelector: [(s) => [s.insightLogicRef], (insightLogicRef) => insightLogicRef?.logic.selectors.filters], insight: [(s) => [(state, props) => s.insightSelector?.(state, props)?.(state, props)], (insight) => insight], + filters: [(s) => [(state, props) => s.filtersSelector?.(state, props)?.(state, props)], (filters) => filters], breadcrumbs: [ - (s) => [s.insight, s.insightLogicRef], - (insight, insightLogicRef): Breadcrumb[] => [ + (s) => [ + s.insightLogicRef, + s.insight, + s.filters, + groupsModel.selectors.aggregationLabel, + cohortsModel.selectors.cohortsById, + mathsLogic.selectors.mathDefinitions, + ], + (insightLogicRef, insight, filters, aggregationLabel, cohortsById, mathDefinitions): Breadcrumb[] => [ { key: Scene.SavedInsights, name: 'Product analytics', @@ -96,7 +109,13 @@ export const insightSceneLogic = kea([ }, { key: insight?.short_id || 'new', - name: insight?.name || insight?.derived_name || 'Unnamed', + name: + insight?.name || + summarizeInsight(insight?.query, filters, { + aggregationLabel, + cohortsById, + mathDefinitions, + }), onRename: async (name: string) => { await insightLogicRef?.logic.asyncActions.setInsightMetadata({ name }) }, diff --git a/frontend/src/scenes/insights/summarizeInsight.ts b/frontend/src/scenes/insights/summarizeInsight.ts index ef9e4834854c6..c74293d616e15 100644 --- a/frontend/src/scenes/insights/summarizeInsight.ts +++ b/frontend/src/scenes/insights/summarizeInsight.ts @@ -343,15 +343,14 @@ export interface SummaryContext { export function summarizeInsight( query: Node | undefined | null, - filters: Partial, + filters: Partial | undefined | null, context: SummaryContext ): string { - const hasFilters = Object.keys(filters || {}).length > 0 return isInsightVizNode(query) ? summarizeInsightQuery(query.source, context) : !!query && !isInsightVizNode(query) ? summarizeQuery(query) - : hasFilters + : filters && Object.keys(filters).length > 0 ? summarizeInsightFilters(filters, context) : '' } diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts index ade9799b123d6..6d7c55e88acc1 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.ts @@ -4,6 +4,7 @@ import { actions, afterMount, connect, kea, key, listeners, path, props, reducer import { loaders } from 'kea-loaders' import { beforeUnload, router } from 'kea-router' import api from 'lib/api' +import { sceneLogic } from 'scenes/sceneLogic' import { Scene } from 'scenes/sceneTypes' import { deletePlaylist, @@ -30,7 +31,7 @@ export const sessionRecordingsPlaylistSceneLogic = kea props.shortId), connect({ - values: [cohortsModel, ['cohortsById']], + values: [cohortsModel, ['cohortsById'], sceneLogic, ['activeScene']], }), actions({ updatePlaylist: (properties?: Partial, silent = false) => ({ @@ -126,7 +127,10 @@ export const sessionRecordingsPlaylistSceneLogic = kea ({ - enabled: (newLocation) => values.hasChanges && newLocation?.pathname !== router.values.location.pathname, + enabled: (newLocation) => + values.activeScene === Scene.ReplayPlaylist && + values.hasChanges && + newLocation?.pathname !== router.values.location.pathname, message: 'Leave playlist?\nChanges you made will be discarded.', onConfirm: () => { actions.setFilters(values.playlist?.filters || null)