Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…mon-lib into user-gitops-repo
  • Loading branch information
mukultayal135 committed Feb 9, 2024
2 parents 64adb49 + cd5238e commit 8df4eec
Show file tree
Hide file tree
Showing 22 changed files with 316 additions and 17 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtron-labs/devtron-fe-common-lib",
"version": "0.0.60-beta-3",
"version": "0.0.62-beta-2",
"description": "Supporting common component library",
"main": "dist/index.js",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-arrow-up-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-sort-arrow-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/Common/Api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ServerErrors } from './ServerError'
import { FALLBACK_REQUEST_TIMEOUT, Host, URLS } from './Constants'
import { ResponseType, APIOptions } from './Types'
import { MutableRefObject } from 'react'

const responseMessages = {
100: 'Continue',
Expand Down Expand Up @@ -228,3 +229,23 @@ export const get = (url: string, options?: APIOptions): Promise<ResponseType> =>
export const trash = (url: string, data?: object, options?: APIOptions): Promise<ResponseType> => {
return fetchInTime(url, 'DELETE', data, options)
}

/**
* Aborts the previous request before triggering next request
*/
export const abortPreviousRequests = <T,>(
callback: () => Promise<T>,
abortControllerRef: MutableRefObject<AbortController>,
): Promise<T> => {
abortControllerRef.current.abort()
// eslint-disable-next-line no-param-reassign
abortControllerRef.current = new AbortController()
return callback()
}

/**
* Returns true if the error is due to a aborted request
*/
export const getIsRequestAborted = (error) =>
// The 0 code is common for aborted and blocked requests
error && error.code === 0 && error.message === 'The user aborted a request.'
18 changes: 17 additions & 1 deletion src/Common/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,20 @@ export const MAX_Z_INDEX = 2147483647
export const SELECTED_APPROVAL_TAB_STATE = {
APPROVAL: 'approval',
PENDING: 'pending',
}
}

export enum SortingOrder {
/**
* Ascending order
*/
ASC = 'ASC',
/**
* Descending order
*/
DESC = 'DESC',
}

/**
* Base page size for pagination
*/
export const DEFAULT_BASE_PAGE_SIZE = 20
1 change: 1 addition & 0 deletions src/Common/Hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useSuperAdmin } from './UseSuperAdmin/UseSuperAdmin'
export { useClickOutside } from './UseClickOutside/UseClickOutside'
export { useWindowSize } from './UseWindowSize/UseWindowSize'
export * from './useUrlFilters'
9 changes: 9 additions & 0 deletions src/Common/Hooks/useUrlFilters/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const DEFAULT_PAGE_NUMBER = 1

export const URL_FILTER_KEYS = {
PAGE_SIZE: 'pageSize',
PAGE_NUMBER: 'pageNumber',
SEARCH_KEY: 'searchKey',
SORT_BY: 'sortBy',
SORT_ORDER: 'sortOrder',
} as const
2 changes: 2 additions & 0 deletions src/Common/Hooks/useUrlFilters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as useUrlFilters } from './useUrlFilters'
export type { UseUrlFiltersProps } from './types'
6 changes: 6 additions & 0 deletions src/Common/Hooks/useUrlFilters/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface UseUrlFiltersProps<T> {
/**
* The key on which the sorting should be applied
*/
initialSortKey?: T
}
121 changes: 121 additions & 0 deletions src/Common/Hooks/useUrlFilters/useUrlFilters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useMemo } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { DEFAULT_BASE_PAGE_SIZE, SortingOrder } from '../../Constants'
import { DEFAULT_PAGE_NUMBER, URL_FILTER_KEYS } from './constants'
import { UseUrlFiltersProps } from './types'

const { PAGE_SIZE, PAGE_NUMBER, SEARCH_KEY, SORT_BY, SORT_ORDER } = URL_FILTER_KEYS

/**
* Generic hook for implementing URL based filters.
* eg: pagination, search, sort.
*
* The exposed handlers can be consumed directly without the need for explicit state management
*
* @example Default Usage:
* ```tsx
* const { pageSize, changePage, ...rest } = useUrlFilters()
* ```
*
* @example Usage with custom type for sort keys and initial sort key:
* ```tsx
* const { sortBy, sortOrder } = useUrlFilters<'email' | 'name'>({ initialSortKey: 'email' })
* ```
*
*/
const useUrlFilters = <T = string>({ initialSortKey }: UseUrlFiltersProps<T> = {}) => {
const location = useLocation()
const history = useHistory()
const searchParams = new URLSearchParams(location.search)

const { pageSize, pageNumber, searchKey, sortBy, sortOrder } = useMemo(() => {
const _pageSize = searchParams.get(PAGE_SIZE)
const _pageNumber = searchParams.get(PAGE_NUMBER)
const _searchKey = searchParams.get(SEARCH_KEY)
const _sortOrder = searchParams.get(SORT_ORDER) as SortingOrder
const _sortBy = searchParams.get(SORT_BY)

const sortByKey = (_sortBy || initialSortKey || '') as T
// Fallback to ascending order
const sortByOrder = Object.values(SortingOrder).includes(_sortOrder) ? _sortOrder : SortingOrder.ASC

return {
pageSize: Number(_pageSize) || DEFAULT_BASE_PAGE_SIZE,
pageNumber: Number(_pageNumber) || DEFAULT_PAGE_NUMBER,
searchKey: _searchKey || '',
sortBy: sortByKey,
// sort order should only be applied if the key is available
sortOrder: (sortByKey ? sortByOrder : '') as SortingOrder,
}
}, [searchParams])

/**
* Used for getting the required result from the API
*/
const offset = pageSize * (pageNumber - 1)

/**
* Update and replace the search params in the URL.
*
* Note: Currently only primitive data types are supported
*/
const _updateSearchParam = (key: string, value) => {
searchParams.set(key, String(value))
history.replace({ search: searchParams.toString() })
}

const _resetPageNumber = () => {
_updateSearchParam(PAGE_NUMBER, DEFAULT_PAGE_NUMBER)
}

const changePage = (page: number) => {
_updateSearchParam(PAGE_NUMBER, page)
}

const changePageSize = (_pageSize: number) => {
_updateSearchParam(PAGE_SIZE, _pageSize)
_resetPageNumber()
}

const handleSearch = (searchTerm: string) => {
_updateSearchParam(SEARCH_KEY, searchTerm)
_resetPageNumber()
}

const handleSorting = (_sortBy: T) => {
let order: SortingOrder
if (_sortBy === sortBy && sortOrder === SortingOrder.ASC) {
order = SortingOrder.DESC
} else {
order = SortingOrder.ASC
}

_updateSearchParam(SORT_BY, _sortBy)
_updateSearchParam(SORT_ORDER, order)

// Reset page number on sorting change
_resetPageNumber()
}

const clearFilters = () => {
Object.values(URL_FILTER_KEYS).forEach((key) => {
searchParams.delete(key)
})
history.replace({ search: searchParams.toString() })
}

return {
pageSize,
changePage,
changePageSize,
searchKey,
handleSearch,
offset,
sortBy,
sortOrder,
handleSorting,
clearFilters,
}
}

export default useUrlFilters
34 changes: 23 additions & 11 deletions src/Common/SearchBar/SearchBar.component.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeEvent, useCallback, useRef, useState, KeyboardEvent } from 'react'
import { ChangeEvent, useCallback, useRef, useState, KeyboardEvent, useEffect } from 'react'
import { ReactComponent as Search } from '../../Assets/Icon/ic-search.svg'
import { ReactComponent as Clear } from '../../Assets/Icon/ic-error-cross.svg'
import { SearchBarProps } from './types'
Expand Down Expand Up @@ -55,35 +55,46 @@ const SearchBar = ({
debounceTimeout,
])

// assuming initialSearchText will change if we are changing history otherwise will be constant and will not change
// since on changing history we expect to make api call using useAsync so not applying handleEnter
useEffect(() => {
inputRef.current.value = initialSearchText
setShowClearButton(!!initialSearchText)
}, [initialSearchText])

const _applySearch = (value: string) => {
handleSearchChange(value)
handleEnter(value)
}

const clearSearch = () => {
inputRef.current.value = ''
_applySearch('')
setShowClearButton(false)
}

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target
setShowClearButton(!!value)

if (shouldDebounce) {
debouncedSearchChange(value)
} else {
handleSearchChange(value)
}
}

const _applySearch = (value: string) => {
handleSearchChange(value)
handleEnter(value)
}

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const { key } = e

if (key === 'Enter') {
e.preventDefault()
const inputTarget = e.target as HTMLInputElement
const value = inputTarget.value.trim()
_applySearch(value)
}
}

const clearSearch = () => {
inputRef.current.value = ''
_applySearch('')
}
return (
<div className={containerClassName}>
<div className="search-bar bc-n50 focus-within-border-b5 dc__hover-border-n300 dc__block w-100 min-w-200 dc__position-rel en-2 bw-1 br-4 h-32">
Expand All @@ -94,13 +105,14 @@ const SearchBar = ({
type="text"
{...inputProps}
defaultValue={initialSearchText}
className={`search-bar__input dc__position-abs w-100 h-100 br-4 dc__no-border pt-6 pr-10 pb-6 pl-30 fs-13 lh-20 fw-4 cn-9 placeholder-cn5 ${
className={`search-bar__input bc-n50 dc__position-abs w-100 h-100 br-4 dc__no-border pt-6 pr-10 pb-6 pl-30 fs-13 lh-20 fw-4 cn-9 placeholder-cn5 ${
showClearButton ? 'pr-30' : 'pr-10'
}`}
onChange={handleChange}
onKeyDown={handleKeyDown}
ref={inputRef}
/>
{/* TODO: Sync with product since it should have ic-enter in case of not applied */}
{showClearButton && (
<button
className="flex search-bar__clear-button dc__position-abs dc__transparent mt-0 mb-0 mr-5 ml-5"
Expand Down
44 changes: 44 additions & 0 deletions src/Common/SortableTableHeaderCell/SortableTableHeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ReactComponent as SortIcon } from '../../Assets/Icon/ic-arrow-up-down.svg'
import { ReactComponent as SortArrowDown } from '../../Assets/Icon/ic-sort-arrow-down.svg'
import { SortingOrder } from '../Constants'
import { SortableTableHeaderCellProps } from './types'

/**
* Reusable component for the table header cell with support for sorting icons
*
* @example Usage
* ```tsx
* <SortableTableHeaderCell
* isSorted={currentSortedCell === 'cell'}
* triggerSorting={() => {}}
* sortOrder={SortingOrder.ASC}
* title="Header Cell"
* disabled={isDisabled}
* />
* ```
*/
const SortableTableHeaderCell = ({
isSorted,
triggerSorting,
sortOrder,
title,
disabled,
}: SortableTableHeaderCellProps) => (
<button
type="button"
className="dc__transparent p-0 bcn-0 cn-7 flex dc__content-start dc__gap-4 cursor"
onClick={triggerSorting}
disabled={disabled}
>
<span className="dc__uppercase dc__ellipsis-right">{title}</span>
{isSorted ? (
<SortArrowDown
className={`icon-dim-12 mw-12 scn-7 ${sortOrder === SortingOrder.DESC ? 'dc__flip-180' : ''}`}
/>
) : (
<SortIcon className="icon-dim-12 mw-12 scn-7" />
)}
</button>
)

export default SortableTableHeaderCell
2 changes: 2 additions & 0 deletions src/Common/SortableTableHeaderCell/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as SortableTableHeaderCell } from './SortableTableHeaderCell'
export type { SortableTableHeaderCellProps } from './types'
26 changes: 26 additions & 0 deletions src/Common/SortableTableHeaderCell/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SortingOrder } from '../Constants'

export interface SortableTableHeaderCellProps {
/**
* If true, the cell is sorted
*/
isSorted: boolean
/**
* Callback for handling the sorting of the cell
*/
triggerSorting: () => void
/**
* Current sort order
*
* Note: On click, the sort order should be updated as required
*/
sortOrder: SortingOrder
/**
* Label for the cell
*/
title: string
/**
* If true, the cell is disabled
*/
disabled: boolean
}
Loading

0 comments on commit 8df4eec

Please sign in to comment.