Skip to content

Commit 715f9f4

Browse files
authored
Merge pull request #538 from devtron-labs/feat/fe-enhancements
feat: configurable history stack for filters, UX updates
2 parents 7b1a832 + 78da302 commit 715f9f4

File tree

13 files changed

+159
-53
lines changed

13 files changed

+159
-53
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "1.6.5",
3+
"version": "1.6.6",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",
@@ -70,7 +70,7 @@
7070
"sharp": "^0.33.5",
7171
"svgo": "^3.3.2",
7272
"typescript": "5.5.4",
73-
"vite": "5.4.11",
73+
"vite": "5.4.14",
7474
"vite-plugin-dts": "4.0.3",
7575
"vite-plugin-image-optimizer": "^1.1.8",
7676
"vite-plugin-lib-inject-css": "2.1.1",
@@ -127,7 +127,7 @@
127127
"react-dom": "^17.0.2"
128128
},
129129
"vite-plugin-svgr": {
130-
"vite": "5.4.11"
130+
"vite": "5.4.14"
131131
},
132132
"react-virtualized-sticky-tree": {
133133
"react": "^17.0.2",

src/Common/Hooks/useUrlFilters/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,20 @@ export interface UseUrlFiltersProps<T, K> {
2525
* Callback function for parsing the search params
2626
*/
2727
parseSearchParams?: (searchParams: URLSearchParams) => K
28+
/**
29+
* Local storage key to store the filters
30+
*/
2831
localStorageKey?: `${string}__${string}`
32+
/**
33+
* Method to be used for redirection in case of filter update
34+
*
35+
* @default 'replace'
36+
*/
37+
redirectionMethod?: 'replace' | 'push'
2938
}
3039

40+
export type UpdateSearchParamsOptionsType<T, K = unknown> = Partial<Pick<UseUrlFiltersProps<T, K>, 'redirectionMethod'>>
41+
3142
export type UseUrlFiltersReturnType<T, K = unknown> = K & {
3243
/**
3344
* Currently applied page size
@@ -74,5 +85,5 @@ export type UseUrlFiltersReturnType<T, K = unknown> = K & {
7485
/**
7586
* Update the search params with the passed object
7687
*/
77-
updateSearchParams: (paramsToSerialize: Partial<K>) => void
88+
updateSearchParams: (paramsToSerialize: Partial<K>, options?: UpdateSearchParamsOptionsType<T, K>) => void
7889
}

src/Common/Hooks/useUrlFilters/useUrlFilters.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useHistory, useLocation } from 'react-router-dom'
1919
import { getUrlWithSearchParams } from '@Common/Helper'
2020
import { DEFAULT_BASE_PAGE_SIZE, EXCLUDED_FALSY_VALUES, SortingOrder } from '../../Constants'
2121
import { DEFAULT_PAGE_NUMBER, URL_FILTER_KEYS } from './constants'
22-
import { UseUrlFiltersProps, UseUrlFiltersReturnType } from './types'
22+
import { UpdateSearchParamsOptionsType, UseUrlFiltersProps, UseUrlFiltersReturnType } from './types'
2323
import { setItemInLocalStorageIfKeyExists } from './utils'
2424

2525
const { PAGE_SIZE, PAGE_NUMBER, SEARCH_KEY, SORT_BY, SORT_ORDER } = URL_FILTER_KEYS
@@ -45,6 +45,7 @@ const useUrlFilters = <T = string, K = unknown>({
4545
initialSortKey,
4646
parseSearchParams,
4747
localStorageKey,
48+
redirectionMethod = 'replace',
4849
}: UseUrlFiltersProps<T, K> = {}): UseUrlFiltersReturnType<T, K> => {
4950
const location = useLocation()
5051
const history = useHistory()
@@ -62,6 +63,7 @@ const useUrlFilters = <T = string, K = unknown>({
6263
const localSearchString = getUrlWithSearchParams('', JSON.parse(localStorageValue))
6364
const localSearchParams = new URLSearchParams(localSearchString.split('?')[1] ?? '')
6465

66+
// This would remain replace since the initial value is being set from local storage
6567
history.replace({ search: localSearchParams.toString() })
6668
return localSearchParams
6769
} catch {
@@ -118,23 +120,34 @@ const useUrlFilters = <T = string, K = unknown>({
118120
*/
119121
const offset = pageSize * (pageNumber - 1)
120122

123+
const updateFiltersInUrl = (updatedSearchString: string, options: UpdateSearchParamsOptionsType<T, K>) => {
124+
const params = { search: updatedSearchString }
125+
const methodForRedirection = options.redirectionMethod ?? redirectionMethod
126+
127+
if (methodForRedirection === 'push') {
128+
history.push(params)
129+
} else {
130+
history.replace(params)
131+
}
132+
}
133+
121134
/**
122135
* Update and replace the search params in the URL.
123136
*
124137
* Note: Currently only primitive data types are supported
125138
*/
126-
const _updateSearchParam = (key: string, value) => {
139+
const _updateSearchParam = (key: string, value: unknown, options: UpdateSearchParamsOptionsType<T, K> = {}) => {
127140
searchParams.set(key, String(value))
128-
history.replace({ search: searchParams.toString() })
141+
updateFiltersInUrl(searchParams.toString(), options)
129142
}
130143

131-
const _resetPageNumber = () => {
144+
const _resetPageNumber = (options: UpdateSearchParamsOptionsType<T, K> = {}) => {
132145
if (pageNumber !== DEFAULT_PAGE_NUMBER) {
133-
_updateSearchParam(PAGE_NUMBER, DEFAULT_PAGE_NUMBER)
146+
_updateSearchParam(PAGE_NUMBER, DEFAULT_PAGE_NUMBER, options)
134147
return
135148
}
136149

137-
history.replace({ search: searchParams.toString() })
150+
updateFiltersInUrl(searchParams.toString(), options)
138151
}
139152

140153
const changePage = (page: number) => {
@@ -167,13 +180,13 @@ const useUrlFilters = <T = string, K = unknown>({
167180
}
168181

169182
const clearFilters = () => {
170-
history.replace({ search: '' })
183+
updateFiltersInUrl('', {})
171184
if (localStorageKey) {
172185
localStorage.removeItem(localStorageKey)
173186
}
174187
}
175188

176-
const updateSearchParams = (paramsToSerialize: Partial<K>) => {
189+
const updateSearchParams = (paramsToSerialize: Partial<K>, options: UpdateSearchParamsOptionsType<T, K> = {}) => {
177190
Object.keys(paramsToSerialize).forEach((key) => {
178191
if (!EXCLUDED_FALSY_VALUES.includes(paramsToSerialize[key])) {
179192
if (Array.isArray(paramsToSerialize[key])) {
@@ -191,7 +204,7 @@ const useUrlFilters = <T = string, K = unknown>({
191204
// Skipping primary params => pageSize, pageNumber, searchKey, sortBy, sortOrder
192205
setItemInLocalStorageIfKeyExists(localStorageKey, JSON.stringify(getParsedSearchParams(searchParams)))
193206
// Not replacing the params as it is being done by _resetPageNumber
194-
_resetPageNumber()
207+
_resetPageNumber(options)
195208
}
196209

197210
return {

src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const SortableTableHeaderCell = ({
7676
if (isSorted) {
7777
return (
7878
<SortArrowDown
79-
className={`icon-dim-12 mw-12 scn-7 dc__no-shrink dc__transition--transform ${sortOrder === SortingOrder.DESC ? 'dc__flip-180' : ''}`}
79+
className={`icon-dim-12 mw-12 scn-7 dc__no-shrink dc__transition--transform ${sortOrder !== SortingOrder.DESC ? 'dc__flip-180' : ''}`}
8080
/>
8181
)
8282
}
@@ -99,6 +99,7 @@ const SortableTableHeaderCell = ({
9999
onClick={isSortable ? triggerSorting : noop}
100100
disabled={disabled}
101101
tabIndex={disabled || !isSortable ? -1 : 0}
102+
data-testid={title}
102103
>
103104
<Tooltip showOnTruncate={showTippyOnTruncate} content={title}>
104105
<span className="dc__uppercase dc__truncate">{title}</span>

src/Shared/Components/Button/Button.component.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { ButtonHTMLAttributes, PropsWithChildren } from 'react'
17+
import { ButtonHTMLAttributes, MutableRefObject, PropsWithChildren, useEffect, useRef, useState } from 'react'
1818
import { Link, LinkProps } from 'react-router-dom'
1919
import { Progressing } from '@Common/Progressing'
2020
import { Tooltip } from '@Common/Tooltip'
@@ -29,6 +29,7 @@ const ButtonElement = ({
2929
linkProps,
3030
buttonProps,
3131
onClick,
32+
elementRef,
3233
...props
3334
}: PropsWithChildren<
3435
Omit<
@@ -49,6 +50,7 @@ const ButtonElement = ({
4950
className: string
5051
'data-testid': ButtonProps['dataTestId']
5152
'aria-label': ButtonProps['ariaLabel']
53+
elementRef: MutableRefObject<HTMLButtonElement | HTMLAnchorElement>
5254
}
5355
>) => {
5456
if (component === ButtonComponentType.link) {
@@ -59,6 +61,7 @@ const ButtonElement = ({
5961
// Added the specific class to ensure that the link override is applied
6062
className={`${props.className} button__link ${props.disabled ? 'dc__disable-click' : ''}`}
6163
onClick={onClick as LinkProps['onClick']}
64+
ref={elementRef as MutableRefObject<HTMLAnchorElement>}
6265
/>
6366
)
6467
}
@@ -70,6 +73,7 @@ const ButtonElement = ({
7073
// eslint-disable-next-line react/button-has-type
7174
type={buttonProps?.type || 'button'}
7275
onClick={onClick as ButtonHTMLAttributes<HTMLButtonElement>['onClick']}
76+
ref={elementRef as MutableRefObject<HTMLButtonElement>}
7377
/>
7478
)
7579
}
@@ -155,14 +159,46 @@ const Button = ({
155159
showAriaLabelInTippy = true,
156160
fullWidth = false,
157161
isOpacityHoverChild = false,
162+
triggerAutoClickTimestamp,
158163
...props
159164
}: ButtonProps) => {
165+
const [isAutoClickActive, setIsAutoClickActive] = useState(false)
166+
const autoClickTimeoutRef = useRef<number>()
167+
const elementRef = useRef<HTMLButtonElement | HTMLAnchorElement>(null)
168+
160169
const isDisabled = disabled || isLoading
161170
const iconClass = `dc__no-shrink flex dc__fill-available-space ${getButtonIconClassName({
162171
size,
163172
icon,
164173
})}`
165174

175+
useEffect(() => {
176+
if (triggerAutoClickTimestamp) {
177+
// Adding after timeout to ensure the transition is triggered after the button is rendered
178+
setTimeout(() => {
179+
setIsAutoClickActive(true)
180+
181+
autoClickTimeoutRef.current = setTimeout(() => {
182+
elementRef.current.click()
183+
// This is 100ms less than the duration of the transition in CSS
184+
// Make sure to update the same in CSS if this is changed
185+
}, 2400)
186+
}, 100)
187+
}
188+
189+
return () => {
190+
setIsAutoClickActive(false)
191+
clearTimeout(autoClickTimeoutRef.current)
192+
}
193+
}, [triggerAutoClickTimestamp])
194+
195+
const handleClick: ButtonProps['onClick'] = (e) => {
196+
setIsAutoClickActive(false)
197+
clearTimeout(autoClickTimeoutRef.current)
198+
199+
props.onClick?.(e)
200+
}
201+
166202
const getTooltipProps = (): TooltipProps => {
167203
// Show the aria label as tippy only if the action based tippy is not to be shown
168204
if (!showTooltip && showAriaLabelInTippy && icon && ariaLabel) {
@@ -189,9 +225,11 @@ const Button = ({
189225
<ButtonElement
190226
{...props}
191227
disabled={isDisabled}
192-
className={`br-4 flex cursor dc__tab-focus dc__position-rel dc__capitalize ${isOpacityHoverChild ? 'dc__opacity-hover--child' : ''} ${getButtonDerivedClass({ size, variant, style, isLoading, icon })} ${isDisabled ? 'dc__disabled' : ''} ${fullWidth ? 'w-100' : ''}`}
228+
className={`br-4 flex cursor dc__tab-focus dc__position-rel dc__capitalize ${isOpacityHoverChild ? 'dc__opacity-hover--child' : ''} ${getButtonDerivedClass({ size, variant, style, isLoading, icon, isAutoTriggerActive: isAutoClickActive })} ${isDisabled ? 'dc__disabled' : ''} ${fullWidth ? 'w-100' : ''}`}
193229
data-testid={dataTestId}
194230
aria-label={ariaLabel}
231+
elementRef={elementRef}
232+
onClick={handleClick}
195233
>
196234
{icon ? (
197235
<span className={iconClass}>{icon}</span>

0 commit comments

Comments
 (0)