Skip to content

Commit 8f3121a

Browse files
feat: Control position of shortcuts on new sidebar (#42623)
We can now control whether they go on top or bottom. I squeezed some stuff in this commit too such as resetting the cache for pinned folder - need this.
1 parent f59e775 commit 8f3121a

File tree

29 files changed

+278
-69
lines changed

29 files changed

+278
-69
lines changed

frontend/src/layout/panel-layout/PinnedFolder/EditCustomProductsModal.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { useActions, useValues } from 'kea'
33
import { LemonButton } from 'lib/lemon-ui/LemonButton'
44
import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox'
55
import { LemonModal } from 'lib/lemon-ui/LemonModal'
6+
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
67
import { LemonTag } from 'lib/lemon-ui/LemonTag'
78
import { Spinner } from 'lib/lemon-ui/Spinner'
89

910
import { iconForType } from '~/layout/panel-layout/ProjectTree/defaultTree'
1011
import { FileSystemIconType, FileSystemImport } from '~/queries/schema/schema-general'
12+
import { UserShortcutPosition } from '~/types'
1113

1214
import { editCustomProductsModalLogic } from './editCustomProductsModalLogic'
1315

@@ -19,11 +21,13 @@ export function EditCustomProductsModal(): JSX.Element {
1921
selectedPaths,
2022
allowSidebarSuggestions,
2123
sidebarSuggestionsLoading,
24+
shortcutPosition,
25+
shortcutPositionLoading,
2226
categories,
2327
productsByCategory,
2428
productLoading,
2529
} = useValues(editCustomProductsModalLogic)
26-
const { toggleProduct, toggleCategory, toggleSidebarSuggestions, closeModal } =
30+
const { toggleProduct, toggleCategory, toggleSidebarSuggestions, setShortcutPosition, closeModal } =
2731
useActions(editCustomProductsModalLogic)
2832

2933
return (
@@ -148,6 +152,26 @@ export function EditCustomProductsModal(): JSX.Element {
148152
You can always remove these suggestions later.
149153
</span>
150154
</div>
155+
156+
<div className="flex flex-col items-start gap-2 border-t pt-4">
157+
<div className="flex flex-col gap-2 w-full">
158+
<label className="text-sm font-semibold text-tertiary">Shortcut position</label>
159+
<LemonSelect<UserShortcutPosition>
160+
value={shortcutPosition}
161+
onChange={(value) => setShortcutPosition(value)}
162+
options={[
163+
{ label: 'Above products', value: 'above' },
164+
{ label: 'Below products', value: 'below' },
165+
{ label: 'Hidden', value: 'hidden' },
166+
]}
167+
disabledReason={shortcutPositionLoading ? 'Saving...' : undefined}
168+
fullWidth
169+
/>
170+
<span className="text-sm text-muted">
171+
Choose where shortcuts appear in your sidebar when using custom products.
172+
</span>
173+
</div>
174+
</div>
151175
</div>
152176
</LemonModal>
153177
)

frontend/src/layout/panel-layout/PinnedFolder/PinnedFolder.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export function PinnedFolder(): JSX.Element {
3939

4040
const showDefaultHeader = !['products://', 'data://', 'custom-products://'].includes(pinnedFolder)
4141

42+
const isCustomProductsSidebarEnabled = featureFlags[FEATURE_FLAGS.CUSTOM_PRODUCTS_SIDEBAR] === 'test'
43+
const CustomProductsIcon = isCustomProductsSidebarEnabled ? IconGear : IconPencil
44+
4245
const configMenu = (
4346
<>
4447
{pinnedFolder === 'shortcuts://' ? (
@@ -60,11 +63,11 @@ export function PinnedFolder(): JSX.Element {
6063
onClick={openEditCustomProductsModal}
6164
size="xs"
6265
>
63-
<IconPencil className="size-3 text-secondary" />
66+
<CustomProductsIcon className="size-3 text-secondary" />
6467
</ButtonPrimitive>
6568
) : null}
6669

67-
{featureFlags[FEATURE_FLAGS.CUSTOM_PRODUCTS_SIDEBAR] !== 'test' && (
70+
{!isCustomProductsSidebarEnabled && (
6871
<DropdownMenu>
6972
<DropdownMenuTrigger asChild>
7073
<ButtonPrimitive

frontend/src/layout/panel-layout/PinnedFolder/editCustomProductsModalLogic.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { userLogic } from 'scenes/userLogic'
99
import { customProductsLogic } from '~/layout/panel-layout/ProjectTree/customProductsLogic'
1010
import { getDefaultTreeProducts } from '~/layout/panel-layout/ProjectTree/defaultTree'
1111
import { FileSystemImport } from '~/queries/schema/schema-general'
12+
import { UserShortcutPosition } from '~/types'
1213

1314
import type { editCustomProductsModalLogicType } from './editCustomProductsModalLogicType'
1415

@@ -34,9 +35,11 @@ export const editCustomProductsModalLogic = kea<editCustomProductsModalLogicType
3435
toggleProduct: (productPath: string) => ({ productPath }),
3536
toggleCategory: (category: string) => ({ category }),
3637
setAllowSidebarSuggestions: (value: boolean) => ({ value }),
38+
setShortcutPosition: (value: UserShortcutPosition, saveToUser: boolean = true) => ({ value, saveToUser }),
3739
setSelectedPaths: (paths: Set<string>) => ({ paths }),
3840
setProductLoading: (productPath: string, loading: boolean) => ({ productPath, loading }),
3941
setSidebarSuggestionsLoading: (loading: boolean) => ({ loading }),
42+
setShortcutPositionLoading: (loading: boolean) => ({ loading }),
4043
toggleSidebarSuggestions: true,
4144
openModal: true,
4245
closeModal: true,
@@ -78,6 +81,18 @@ export const editCustomProductsModalLogic = kea<editCustomProductsModalLogicType
7881
toggleSidebarSuggestions: () => true,
7982
},
8083
],
84+
shortcutPosition: [
85+
'above' as UserShortcutPosition,
86+
{
87+
setShortcutPosition: (_, { value }) => value,
88+
},
89+
],
90+
shortcutPositionLoading: [
91+
false,
92+
{
93+
setShortcutPositionLoading: (_, { loading }) => loading,
94+
},
95+
],
8196
productLoading: [
8297
{} as Record<string, boolean>,
8398
{
@@ -132,6 +147,7 @@ export const editCustomProductsModalLogic = kea<editCustomProductsModalLogicType
132147
loadUserSuccess: ({ user }) => {
133148
if (user) {
134149
actions.setAllowSidebarSuggestions(user.allow_sidebar_suggestions ?? false)
150+
actions.setShortcutPosition((user.shortcut_position ?? 'above') as UserShortcutPosition, false)
135151
}
136152
},
137153
toggleProduct: async ({ productPath }) => {
@@ -198,10 +214,26 @@ export const editCustomProductsModalLogic = kea<editCustomProductsModalLogicType
198214
actions.setSidebarSuggestionsLoading(false)
199215
}
200216
},
217+
setShortcutPosition: async ({ value, saveToUser }) => {
218+
if (!saveToUser) {
219+
return
220+
}
221+
222+
try {
223+
actions.setShortcutPositionLoading(true)
224+
actions.updateUser({ shortcut_position: value })
225+
} catch (error) {
226+
console.error('Failed to save shortcut position preference:', error)
227+
lemonToast.error('Failed to save preference. Try again?')
228+
} finally {
229+
actions.setShortcutPositionLoading(false)
230+
}
231+
},
201232
})),
202233
afterMount(({ actions, values }) => {
203234
if (values.user) {
204235
actions.setAllowSidebarSuggestions(values.user.allow_sidebar_suggestions ?? false)
236+
actions.setShortcutPosition((values.user.shortcut_position ?? 'above') as UserShortcutPosition, false)
205237
}
206238

207239
if (values.customProducts.length > 0) {

frontend/src/layout/panel-layout/PinnedFolder/pinnedFolderLogic.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { splitProtocolPath } from '~/layout/panel-layout/ProjectTree/utils'
99

1010
import type { pinnedFolderLogicType } from './pinnedFolderLogicType'
1111

12-
const LOCAL_STORAGE_PINNED_FOLDER_KEY = 'layout.panel-layout.PinnedFolder.pinnedFolderLogic.lazyLoaders.pinnedFolder'
12+
const LOCAL_STORAGE_PINNED_FOLDER_KEY = 'layout.panel-layout.PinnedFolder.pinnedFolderLogic.lazyLoaders.pinnedFolder.v2'
1313

1414
export const pinnedFolderLogic = kea<pinnedFolderLogicType>([
1515
path(['layout', 'panel-layout', 'PinnedFolder', 'pinnedFolderLogic']),

frontend/src/layout/panel-layout/ProjectTree/ProjectTree.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
IconChevronRight,
88
IconEllipsis,
99
IconFolderPlus,
10+
IconGear,
1011
IconPencil,
1112
IconPlusSmall,
1213
IconShortcut,
@@ -15,6 +16,7 @@ import {
1516
import { itemSelectModalLogic } from 'lib/components/FileSystem/ItemSelectModal/itemSelectModalLogic'
1617
import { ResizableElement } from 'lib/components/ResizeElement/ResizeElement'
1718
import { dayjs } from 'lib/dayjs'
19+
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
1820
import { useLocalStorage } from 'lib/hooks/useLocalStorage'
1921
import { LemonTag } from 'lib/lemon-ui/LemonTag'
2022
import { LemonTree, LemonTreeRef, LemonTreeSize, TreeDataItem } from 'lib/lemon-ui/LemonTree/LemonTree'
@@ -123,6 +125,8 @@ export function ProjectTree({
123125
const { setProjectTreeMode } = useActions(projectTreeLogic({ key: PROJECT_TREE_KEY }))
124126
const { openItemSelectModal } = useActions(itemSelectModalLogic)
125127

128+
const isCustomProductsExperiment = useFeatureFlag('CUSTOM_PRODUCTS_SIDEBAR', 'test')
129+
126130
const { customProducts, customProductsLoading } = useValues(customProductsLogic)
127131
const { seed } = useActions(customProductsLogic)
128132

@@ -167,11 +171,14 @@ export function ProjectTree({
167171
}
168172

169173
if (root === 'custom-products://') {
170-
const hasColleagueProducts = customProducts.some(
171-
(item) => item.reason === UserProductListReason.USED_BY_COLLEAGUES
174+
const hasRecommendedProducts = customProducts.some(
175+
(item) =>
176+
item.reason === UserProductListReason.USED_BY_COLLEAGUES ||
177+
item.reason === UserProductListReason.USED_ON_SEPARATE_TEAM
172178
)
173179

174180
if (fullFileSystemFiltered.length === 0 || !customProductHelperDismissed) {
181+
const CustomIcon = isCustomProductsExperiment ? IconGear : IconPencil
175182
treeData.push({
176183
id: 'products/custom-products-helper-category',
177184
name: 'Example custom products',
@@ -184,7 +191,7 @@ export function ProjectTree({
184191
>
185192
You can display your preferred apps here. You can configure what items show up in here by
186193
clicking on the{' '}
187-
<IconPencil className="size-3 border border-[var(--color-neutral-500)] rounded-xs" /> icon
194+
<CustomIcon className="size-3 border border-[var(--color-neutral-500)] rounded-xs" /> icon
188195
above. We'll automatically suggest new apps to this list as you use them.{' '}
189196
{fullFileSystemFiltered.length > 0 && (
190197
<span
@@ -196,7 +203,7 @@ export function ProjectTree({
196203
)}
197204
<br />
198205
<br />
199-
{!hasColleagueProducts && fullFileSystemFiltered.length <= 3 && (
206+
{!hasRecommendedProducts && fullFileSystemFiltered.length <= 3 && (
200207
<span className="cursor-pointer underline" onClick={seed}>
201208
{customProductsLoading ? 'Adding...' : 'Add recommended products?'}
202209
</span>

frontend/src/layout/panel-layout/ProjectTree/projectTreeDataLogic.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
1313
import { capitalizeFirstLetter, humanList, identifierToHuman, pluralize } from 'lib/utils'
1414
import { getCurrentTeamIdOrNone } from 'lib/utils/getAppContext'
1515
import { urls } from 'scenes/urls'
16+
import { userLogic } from 'scenes/userLogic'
1617

1718
import { breadcrumbsLogic } from '~/layout/navigation/Breadcrumbs/breadcrumbsLogic'
1819
import {
@@ -36,7 +37,7 @@ import {
3637
import { FEATURE_FLAGS } from '~/lib/constants'
3738
import { groupsModel } from '~/models/groupsModel'
3839
import { FileSystemEntry, FileSystemIconType, FileSystemImport } from '~/queries/schema/schema-general'
39-
import { UserBasicType } from '~/types'
40+
import { UserBasicType, UserShortcutPosition } from '~/types'
4041

4142
import { panelLayoutLogic } from '../panelLayoutLogic'
4243
import { customProductsLogic } from './customProductsLogic'
@@ -141,6 +142,8 @@ export const projectTreeDataLogic = kea<projectTreeDataLogicType>([
141142
['aggregationLabel', 'groupTypes', 'groupTypesLoading', 'groupsAccessStatus'],
142143
customProductsLogic,
143144
['customProducts'],
145+
userLogic,
146+
['user'],
144147
],
145148
actions: [panelLayoutLogic, ['setActivePanelIdentifier']],
146149
})),
@@ -941,13 +944,14 @@ export const projectTreeDataLogic = kea<projectTreeDataLogicType>([
941944
new Set(shortcutData.filter((shortcut) => shortcut.type !== 'folder').map((shortcut) => shortcut.path)),
942945
],
943946
getCustomProductTreeItems: [
944-
(s) => [s.customProducts, s.featureFlags, s.getShortcutTreeItems, s.folderStates, s.users],
947+
(s) => [s.customProducts, s.featureFlags, s.getShortcutTreeItems, s.folderStates, s.users, s.user],
945948
(
946949
customProducts,
947950
featureFlags,
948951
getShortcutTreeItems,
949952
folderStates,
950-
users
953+
users,
954+
user
951955
): ((searchTerm: string) => TreeDataItem[]) => {
952956
return function getCustomProductItems(searchTerm: string): TreeDataItem[] {
953957
const allProducts = getDefaultTreeProducts()
@@ -986,22 +990,29 @@ export const projectTreeDataLogic = kea<projectTreeDataLogicType>([
986990
searchTerm,
987991
})
988992

989-
const result: TreeDataItem[] = []
993+
const shortcutPosition = (user?.shortcut_position ?? 'above') as UserShortcutPosition
994+
const generateShortcutItemsCategory = (): TreeDataItem[] => {
995+
const shortcutItems = getShortcutTreeItems(searchTerm, false)
996+
if (shortcutItems.length === 0) {
997+
return []
998+
}
990999

991-
// Shortcuts above the custom products to keep them all together here
992-
const shortcutItems = getShortcutTreeItems(searchTerm, false)
993-
if (shortcutItems.length > 0) {
994-
result.push({
995-
id: 'custom-products://-shortcuts-category',
996-
name: 'Shortcuts',
997-
displayName: <>Shortcuts</>,
998-
type: 'category',
999-
})
1000-
result.push(...shortcutItems)
1000+
return [
1001+
{
1002+
id: 'custom-products://-shortcuts-category',
1003+
name: 'Shortcuts',
1004+
displayName: <>Shortcuts</>,
1005+
type: 'category',
1006+
},
1007+
...shortcutItems,
1008+
]
10011009
}
10021010

1003-
const customProductItems = convert(selectedProducts, 'custom-products://')
1004-
result.push(...customProductItems)
1011+
const result: TreeDataItem[] = [
1012+
...(shortcutPosition === 'above' ? generateShortcutItemsCategory() : []),
1013+
...convert(selectedProducts, 'custom-products://'),
1014+
...(shortcutPosition === 'below' ? generateShortcutItemsCategory() : []),
1015+
]
10051016

10061017
return result
10071018
}

frontend/src/lib/api.mock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export const MOCK_DEFAULT_USER: UserType = {
269269
is_2fa_enabled: false,
270270
has_social_auth: false,
271271
has_sso_enforcement: false,
272+
shortcut_position: 'above',
272273
sensitive_session_expires_at: dayjs().add(1, 'hour').toISOString(),
273274
theme_mode: null,
274275
team: MOCK_DEFAULT_TEAM,

frontend/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export interface SceneDashboardChoice {
309309
}
310310

311311
export type UserTheme = 'light' | 'dark' | 'system'
312+
export type UserShortcutPosition = 'above' | 'below' | 'hidden'
312313

313314
/** Full User model. */
314315
export interface UserType extends UserBaseType {
@@ -338,6 +339,7 @@ export interface UserType extends UserBaseType {
338339
is_2fa_enabled: boolean
339340
has_social_auth: boolean
340341
has_sso_enforcement: boolean
342+
shortcut_position: UserShortcutPosition
341343
has_seen_product_intro_for?: Record<string, boolean>
342344
scene_personalisation?: SceneDashboardChoice[]
343345
theme_mode?: UserTheme | null

posthog/api/test/__snapshots__/test_action.ambr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"posthog_user"."toolbar_mode",
2727
"posthog_user"."hedgehog_config",
2828
"posthog_user"."allow_sidebar_suggestions",
29+
"posthog_user"."shortcut_position",
2930
"posthog_user"."events_column_config",
3031
"posthog_user"."email_opt_in"
3132
FROM "posthog_user"
@@ -347,6 +348,7 @@
347348
"posthog_user"."toolbar_mode",
348349
"posthog_user"."hedgehog_config",
349350
"posthog_user"."allow_sidebar_suggestions",
351+
"posthog_user"."shortcut_position",
350352
"posthog_user"."events_column_config",
351353
"posthog_user"."email_opt_in"
352354
FROM "posthog_action"
@@ -395,6 +397,7 @@
395397
"posthog_user"."toolbar_mode",
396398
"posthog_user"."hedgehog_config",
397399
"posthog_user"."allow_sidebar_suggestions",
400+
"posthog_user"."shortcut_position",
398401
"posthog_user"."events_column_config",
399402
"posthog_user"."email_opt_in"
400403
FROM "posthog_user"
@@ -803,6 +806,7 @@
803806
"posthog_user"."toolbar_mode",
804807
"posthog_user"."hedgehog_config",
805808
"posthog_user"."allow_sidebar_suggestions",
809+
"posthog_user"."shortcut_position",
806810
"posthog_user"."events_column_config",
807811
"posthog_user"."email_opt_in"
808812
FROM "posthog_action"
@@ -1016,6 +1020,7 @@
10161020
"posthog_user"."toolbar_mode",
10171021
"posthog_user"."hedgehog_config",
10181022
"posthog_user"."allow_sidebar_suggestions",
1023+
"posthog_user"."shortcut_position",
10191024
"posthog_user"."events_column_config",
10201025
"posthog_user"."email_opt_in"
10211026
FROM "posthog_action"
@@ -1064,6 +1069,7 @@
10641069
"posthog_user"."toolbar_mode",
10651070
"posthog_user"."hedgehog_config",
10661071
"posthog_user"."allow_sidebar_suggestions",
1072+
"posthog_user"."shortcut_position",
10671073
"posthog_user"."events_column_config",
10681074
"posthog_user"."email_opt_in"
10691075
FROM "posthog_user"

0 commit comments

Comments
 (0)