Skip to content

Commit

Permalink
feat: Improved MemberSelect with search (#19439)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Dec 20, 2023
1 parent d9429ff commit 10f251c
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 74 deletions.
Binary file modified frontend/__snapshots__/scenes-app-saved-insights--card-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 124 additions & 0 deletions frontend/src/lib/components/MemberSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { LemonButton, LemonButtonProps, LemonDropdown, LemonInput, ProfilePicture } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { useMemo, useState } from 'react'
import { membersLogic } from 'scenes/organization/membersLogic'

import { UserBasicType } from '~/types'

export type MemberSelectProps = Pick<LemonButtonProps, 'size' | 'type'> & {
defaultLabel?: string
// NOTE: Trying to cover a lot of different cases - if string we assume uuid, if number we assume id
value: UserBasicType | string | number | null
onChange: (value: UserBasicType | null) => void
}

export function MemberSelect({
defaultLabel = 'All users',
value,
onChange,
...buttonProps
}: MemberSelectProps): JSX.Element {
const { meFirstMembers, membersFuse } = useValues(membersLogic)
const [showPopover, setShowPopover] = useState(false)
const [searchTerm, setSearchTerm] = useState('')

const filteredMembers = useMemo(() => {
return searchTerm ? membersFuse.search(searchTerm).map((result) => result.item) : meFirstMembers
}, [searchTerm, meFirstMembers])

const selectedMember = useMemo(() => {
if (!value) {
return null
}
if (typeof value === 'string' || typeof value === 'number') {
const propToCompare = typeof value === 'string' ? 'uuid' : 'id'
return meFirstMembers.find((member) => member.user[propToCompare] === value)?.user ?? `${value}`
}
return value
}, [value, meFirstMembers])

const _onChange = (value: UserBasicType | null): void => {
setShowPopover(false)
onChange(value)
}

return (
<LemonDropdown
closeOnClickInside={false}
visible={showPopover}
sameWidth={false}
actionable
onVisibilityChange={(visible) => setShowPopover(visible)}
overlay={
<div className="max-w-100 space-y-2 overflow-hidden">
<LemonInput
type="search"
placeholder="Search"
autoFocus
value={searchTerm}
onChange={setSearchTerm}
fullWidth
/>
<ul className="space-y-px">
<li>
<LemonButton
status="stealth"
fullWidth
role="menuitem"
size="small"
onClick={() => _onChange(null)}
>
{defaultLabel}
</LemonButton>
</li>

{filteredMembers.map((member) => (
<li key={member.user.uuid}>
<LemonButton
status="stealth"
fullWidth
role="menuitem"
size="small"
icon={
<ProfilePicture
size="md"
name={member.user.first_name}
email={member.user.email}
/>
}
onClick={() => _onChange(member.user)}
>
<span className="flex items-center justify-between gap-2 flex-1">
<span>{member.user.first_name}</span>
<span className="text-muted-alt">
{meFirstMembers[0] === member && `(you)`}
</span>
</span>
</LemonButton>
</li>
))}

{filteredMembers.length === 0 ? (
<div className="p-2 text-muted-alt italic truncate border-t">
{searchTerm ? <span>No matches</span> : <span>No users</span>}
</div>
) : null}
</ul>
</div>
}
>
<LemonButton {...buttonProps}>
{typeof selectedMember === 'string' ? (
selectedMember
) : selectedMember ? (
<span>
{selectedMember.first_name}
{meFirstMembers[0].user.uuid === selectedMember.uuid ? ` (you)` : ''}
</span>
) : (
defaultLabel
)}
</LemonButton>
</LemonDropdown>
)
}
25 changes: 15 additions & 10 deletions frontend/src/lib/lemon-ui/LemonDropdown/LemonDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { MouseEventHandler, useContext, useEffect, useRef, useState } from 'react'
import React, { MouseEventHandler, useContext, useRef, useState } from 'react'

import { Popover, PopoverOverlayContext, PopoverProps } from '../Popover'

Expand Down Expand Up @@ -38,6 +38,8 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
},
ref
) => {
const isControlled = visible !== undefined

const [, parentPopoverLevel] = useContext(PopoverOverlayContext)
const [localVisible, setLocalVisible] = useState(visible ?? false)

Expand All @@ -46,9 +48,12 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R

const effectiveVisible = visible ?? localVisible

useEffect(() => {
onVisibilityChange?.(localVisible)
}, [localVisible, onVisibilityChange])
const setVisible = (value: boolean): void => {
if (!isControlled) {
setLocalVisible(value)
}
onVisibilityChange?.(value)
}

return (
<Popover
Expand All @@ -57,18 +62,18 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
referenceRef={referenceRef}
onClickOutside={(e) => {
if (trigger === 'click') {
setLocalVisible(false)
setVisible(false)
}
onClickOutside?.(e)
}}
onClickInside={(e) => {
e.stopPropagation()
closeOnClickInside && setLocalVisible(false)
closeOnClickInside && setVisible(false)
onClickInside?.(e)
}}
onMouseLeaveInside={(e) => {
if (trigger === 'hover' && !referenceRef.current?.contains(e.relatedTarget as Node)) {
setLocalVisible(false)
setVisible(false)
}
onMouseLeaveInside?.(e)
}}
Expand All @@ -77,7 +82,7 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
>
{React.cloneElement(children, {
onClick: (e: React.MouseEvent): void => {
setLocalVisible((state) => !state)
setVisible(!effectiveVisible)
children.props.onClick?.(e)
if (parentPopoverLevel > -1) {
// If this button is inside another popover, let's not propagate this event so that
Expand All @@ -87,12 +92,12 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
},
onMouseEnter: (): void => {
if (trigger === 'hover') {
setLocalVisible(true)
setVisible(true)
}
},
onMouseLeave: (e: React.MouseEvent): void => {
if (trigger === 'hover' && !floatingRef.current?.contains(e.relatedTarget as Node)) {
setLocalVisible(false)
setVisible(false)
}
},
'aria-haspopup': 'true',
Expand Down
22 changes: 6 additions & 16 deletions frontend/src/scenes/dashboard/dashboards/DashboardsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IconPin, IconPinFilled, IconShare } from '@posthog/icons'
import { LemonInput, LemonSelect } from '@posthog/lemon-ui'
import { LemonInput } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { MemberSelect } from 'lib/components/MemberSelect'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { DashboardPrivilegeLevel } from 'lib/constants'
import { IconCottage, IconLock } from 'lib/lemon-ui/icons'
Expand All @@ -18,7 +19,6 @@ import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
import { DashboardsFilters, dashboardsLogic } from 'scenes/dashboard/dashboards/dashboardsLogic'
import { deleteDashboardLogic } from 'scenes/dashboard/deleteDashboardLogic'
import { duplicateDashboardLogic } from 'scenes/dashboard/duplicateDashboardLogic'
import { membersLogic } from 'scenes/organization/membersLogic'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'
Expand Down Expand Up @@ -56,7 +56,6 @@ export function DashboardsTable({
const { currentTeam } = useValues(teamLogic)
const { showDuplicateDashboardModal } = useActions(duplicateDashboardLogic)
const { showDeleteDashboardModal } = useActions(deleteDashboardLogic)
const { meFirstMembers } = useValues(membersLogic)

const columns: LemonTableColumns<DashboardType> = [
{
Expand Down Expand Up @@ -246,20 +245,11 @@ export function DashboardsTable({
</div>
<div className="flex items-center gap-2">
<span>Created by:</span>
<LemonSelect
options={[
{ value: 'All users' as string, label: 'All Users' },
...meFirstMembers.map((x) => ({
value: x.user.uuid,
label: x.user.first_name,
})),
]}
<MemberSelect
size="small"
value={filters.createdBy}
onChange={(v: any): void => {
setFilters({ createdBy: v })
}}
dropdownMatchSelectWidth={false}
type="secondary"
value={filters.createdBy === 'All users' ? null : filters.createdBy}
onChange={(user) => setFilters({ createdBy: user?.uuid || 'All users' })}
/>
</div>
{extraActions}
Expand Down
20 changes: 5 additions & 15 deletions frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LemonButton, LemonInput, LemonSelect, LemonTag } from '@posthog/lemon-ui'
import { LemonButton, LemonInput, LemonTag } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { MemberSelect } from 'lib/components/MemberSelect'
import { IconDelete, IconEllipsis } from 'lib/lemon-ui/icons'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonMenu } from 'lib/lemon-ui/LemonMenu'
Expand All @@ -9,7 +10,6 @@ import { Link } from 'lib/lemon-ui/Link'
import { useEffect } from 'react'
import { ContainsTypeFilters } from 'scenes/notebooks/NotebooksTable/ContainsTypeFilter'
import { DEFAULT_FILTERS, notebooksTableLogic } from 'scenes/notebooks/NotebooksTable/notebooksTableLogic'
import { membersLogic } from 'scenes/organization/membersLogic'
import { urls } from 'scenes/urls'

import { notebooksModel } from '~/models/notebooksModel'
Expand Down Expand Up @@ -42,7 +42,6 @@ export function NotebooksTable(): JSX.Element {
const { notebooksAndTemplates, filters, notebooksResponseLoading, notebookTemplates, sortValue, pagination } =
useValues(notebooksTableLogic)
const { loadNotebooks, setFilters, setSortValue } = useActions(notebooksTableLogic)
const { meFirstMembers } = useValues(membersLogic)
const { selectNotebook } = useActions(notebookPanelLogic)

useEffect(() => {
Expand Down Expand Up @@ -121,20 +120,11 @@ export function NotebooksTable(): JSX.Element {
<ContainsTypeFilters filters={filters} setFilters={setFilters} />
<div className="flex items-center gap-2">
<span>Created by:</span>
<LemonSelect
options={[
{ value: DEFAULT_FILTERS.createdBy, label: DEFAULT_FILTERS.createdBy },
...meFirstMembers.map((x) => ({
value: x.user.uuid,
label: x.user.first_name,
})),
]}
<MemberSelect
size="small"
type="secondary"
value={filters.createdBy}
onChange={(v): void => {
setFilters({ createdBy: v || DEFAULT_FILTERS.createdBy })
}}
dropdownMatchSelectWidth={false}
onChange={(user) => setFilters({ createdBy: user?.uuid || DEFAULT_FILTERS.createdBy })}
/>
</div>
</div>
Expand Down
22 changes: 5 additions & 17 deletions frontend/src/scenes/saved-insights/SavedInsightsFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IconCalendar } from '@posthog/icons'
import { useActions, useValues } from 'kea'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { MemberSelect } from 'lib/components/MemberSelect'
import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput'
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
import { membersLogic } from 'scenes/organization/membersLogic'
import { INSIGHT_TYPE_OPTIONS } from 'scenes/saved-insights/SavedInsights'
import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic'

Expand All @@ -17,8 +17,6 @@ export function SavedInsightsFilters(): JSX.Element {

const { tab, createdBy, insightType, dateFrom, dateTo, dashboardId, search } = filters

const { meFirstMembers } = useValues(membersLogic)

return (
<div className="flex justify-between gap-2 mb-2 items-center flex-wrap">
<LemonInput
Expand Down Expand Up @@ -76,21 +74,11 @@ export function SavedInsightsFilters(): JSX.Element {
{tab !== SavedInsightsTabs.Yours ? (
<div className="flex items-center gap-2">
<span>Created by:</span>
{/* TODO: Fix issues with user name order due to numbers having priority */}
<LemonSelect
<MemberSelect
size="small"
options={[
{ value: 'All users' as number | 'All users', label: 'All Users' },
...meFirstMembers.map((x) => ({
value: x.user.id,
label: x.user.first_name,
})),
]}
value={createdBy}
onChange={(v: any): void => {
setSavedInsightsFilters({ createdBy: v })
}}
dropdownMatchSelectWidth={false}
type="secondary"
value={createdBy === 'All users' ? null : createdBy}
onChange={(user) => setSavedInsightsFilters({ createdBy: user?.id || 'All users' })}
/>
</div>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { TZLabel } from '@posthog/apps-common'
import { LemonButton, LemonDivider, LemonInput, LemonSelect, LemonTable, Link } from '@posthog/lemon-ui'
import { LemonButton, LemonDivider, LemonInput, LemonTable, Link } from '@posthog/lemon-ui'
import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { MemberSelect } from 'lib/components/MemberSelect'
import { IconCalendar, IconPinFilled, IconPinOutline } from 'lib/lemon-ui/icons'
import { More } from 'lib/lemon-ui/LemonButton/More'
import { LemonTableColumn, LemonTableColumns } from 'lib/lemon-ui/LemonTable'
import { createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
import { membersLogic } from 'scenes/organization/membersLogic'
import { SavedSessionRecordingPlaylistsEmptyState } from 'scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState'
import { urls } from 'scenes/urls'

Expand Down Expand Up @@ -40,7 +40,6 @@ export function SavedSessionRecordingPlaylists({ tab }: SavedSessionRecordingPla
const logic = savedSessionRecordingPlaylistsLogic({ tab })
const { playlists, playlistsLoading, filters, sorting, pagination } = useValues(logic)
const { setSavedPlaylistsFilters, updatePlaylist, duplicatePlaylist, deletePlaylist } = useActions(logic)
const { meFirstMembers } = useValues(membersLogic)

const columns: LemonTableColumns<SessionRecordingPlaylistType> = [
{
Expand Down Expand Up @@ -159,20 +158,11 @@ export function SavedSessionRecordingPlaylists({ tab }: SavedSessionRecordingPla
</div>
<div className="flex items-center gap-2">
<span>Created by:</span>
<LemonSelect
<MemberSelect
size="small"
options={[
{ value: 'All users' as number | 'All users', label: 'All Users' },
...meFirstMembers.map((x) => ({
value: x.user.id,
label: x.user.first_name,
})),
]}
value={filters.createdBy}
onChange={(v: any): void => {
setSavedPlaylistsFilters({ createdBy: v })
}}
dropdownMatchSelectWidth={false}
type="secondary"
value={filters.createdBy === 'All users' ? null : filters.createdBy}
onChange={(user) => setSavedPlaylistsFilters({ createdBy: user?.id || 'All users' })}
/>
</div>
</div>
Expand Down

0 comments on commit 10f251c

Please sign in to comment.