-
Notifications
You must be signed in to change notification settings - Fork 429
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement feature actions dropdown (#3253)
- Loading branch information
1 parent
9b6da57
commit 972f1a3
Showing
7 changed files
with
302 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { RefObject, useCallback, useEffect } from 'react' | ||
|
||
const useOutsideClick = (ref: RefObject<HTMLElement>, handler: () => void) => { | ||
const handleClick = useCallback( | ||
(event: MouseEvent) => { | ||
if (ref.current && !ref.current.contains(event.target as Node)) { | ||
handler() | ||
} | ||
}, | ||
[handler, ref], | ||
) | ||
|
||
useEffect(() => { | ||
document.addEventListener('click', handleClick) | ||
return () => document.removeEventListener('click', handleClick) | ||
}, [ref, handler, handleClick]) | ||
} | ||
|
||
export default useOutsideClick |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import { FC, useCallback, useLayoutEffect, useRef, useState } from 'react' | ||
import classNames from 'classnames' | ||
|
||
import useOutsideClick from 'common/useOutsideClick' | ||
import Utils from 'common/utils/utils' | ||
import Constants from 'common/constants' | ||
import Permission from 'common/providers/Permission' | ||
import Button from './base/forms/Button' | ||
import Icon from './Icon' | ||
|
||
interface FeatureActionProps { | ||
projectId: string | ||
featureIndex: number | ||
readOnly: boolean | ||
isProtected: boolean | ||
hideAudit: boolean | ||
hideRemove: boolean | ||
isCompact?: boolean | ||
onCopyName: () => void | ||
onShowHistory: () => void | ||
onRemove: () => void | ||
} | ||
|
||
type ActionType = 'copy' | 'history' | 'remove' | ||
|
||
function calculateListPosition( | ||
btnEl: HTMLElement, | ||
listEl: HTMLElement, | ||
): { top: number; left: number } { | ||
const listPosition = listEl.getBoundingClientRect() | ||
const btnPosition = btnEl.getBoundingClientRect() | ||
const pageTop = window.visualViewport?.pageTop ?? 0 | ||
return { | ||
left: btnPosition.right - listPosition.width, | ||
top: pageTop + btnPosition.bottom, | ||
} | ||
} | ||
|
||
export const FeatureAction: FC<FeatureActionProps> = ({ | ||
featureIndex, | ||
hideAudit, | ||
hideRemove, | ||
isCompact, | ||
isProtected, | ||
onCopyName, | ||
onRemove, | ||
onShowHistory, | ||
projectId, | ||
readOnly, | ||
}) => { | ||
const [isOpen, setIsOpen] = useState<boolean>(false) | ||
|
||
const btnRef = useRef<HTMLDivElement>(null) | ||
const listRef = useRef<HTMLDivElement>(null) | ||
|
||
const close = useCallback(() => setIsOpen(false), []) | ||
|
||
const handleOutsideClick = useCallback( | ||
() => isOpen && close(), | ||
[close, isOpen], | ||
) | ||
|
||
const handleActionClick = useCallback( | ||
(action: ActionType) => { | ||
if (action === 'copy') { | ||
onCopyName() | ||
} else if (action === 'history') { | ||
onShowHistory() | ||
} else if (action === 'remove') { | ||
onRemove() | ||
} | ||
|
||
close() | ||
}, | ||
[close, onCopyName, onRemove, onShowHistory], | ||
) | ||
|
||
useOutsideClick(listRef, handleOutsideClick) | ||
|
||
useLayoutEffect(() => { | ||
if (!isOpen || !listRef.current || !btnRef.current) return | ||
const listPosition = calculateListPosition(btnRef.current, listRef.current) | ||
listRef.current.style.top = `${listPosition.top}px` | ||
listRef.current.style.left = `${listPosition.left}px` | ||
}, [isOpen]) | ||
|
||
return ( | ||
<div className='feature-action'> | ||
<div ref={btnRef}> | ||
<Button | ||
style={{ | ||
lineHeight: 0, | ||
padding: isCompact ? '0.625rem' : '0.875rem', | ||
}} | ||
className={classNames('btn btn-with-icon', { | ||
'btn-sm': isCompact, | ||
})} | ||
data-test={`feature-action-${featureIndex}`} | ||
onClick={() => setIsOpen(true)} | ||
> | ||
<Icon | ||
name='more-vertical' | ||
width={isCompact ? 16 : 18} | ||
fill='#6837FC' | ||
/> | ||
</Button> | ||
</div> | ||
|
||
{isOpen && ( | ||
<div ref={listRef} className='feature-action__list'> | ||
<div | ||
className='feature-action__item' | ||
onClick={() => handleActionClick('copy')} | ||
> | ||
<Icon name='copy' width={18} fill='#9DA4AE' /> | ||
<span>Copy Feature Name</span> | ||
</div> | ||
|
||
{!hideAudit && ( | ||
<div | ||
className='feature-action__item' | ||
data-test={`feature-history-${featureIndex}`} | ||
onClick={() => handleActionClick('history')} | ||
> | ||
<Icon name='clock' width={18} fill='#9DA4AE' /> | ||
<span>Show History</span> | ||
</div> | ||
)} | ||
|
||
{!hideRemove && ( | ||
<Permission | ||
level='project' | ||
permission='DELETE_FEATURE' | ||
id={projectId} | ||
> | ||
{({ permission: removeFeaturePermission }) => | ||
Utils.renderWithPermission( | ||
removeFeaturePermission, | ||
Constants.projectPermissions('Delete Feature'), | ||
<Tooltip | ||
html | ||
title={ | ||
<div | ||
className={classNames('feature-action__item', { | ||
'feature-action__item_disabled': | ||
!removeFeaturePermission || readOnly || isProtected, | ||
})} | ||
data-test={`remove-feature-btn-${featureIndex}`} | ||
onClick={() => handleActionClick('remove')} | ||
> | ||
<Icon name='trash-2' width={18} fill='#9DA4AE' /> | ||
<span>Remove feature</span> | ||
</div> | ||
} | ||
> | ||
{isProtected && | ||
'<span>This feature has been tagged as <bold>protected</bold>, <bold>permanent</bold>, <bold>do not delete</bold>, or <bold>read only</bold>. Please remove the tag before attempting to delete this flag.</span>'} | ||
</Tooltip>, | ||
) | ||
} | ||
</Permission> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
) | ||
} | ||
|
||
export default FeatureAction |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
972f1a3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
docs – ./docs
docs-git-main-flagsmith.vercel.app
docs.bullet-train.io
docs-flagsmith.vercel.app
docs.flagsmith.com
972f1a3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
flagsmith-frontend-staging – ./frontend
flagsmith-staging-frontend.vercel.app
flagsmith-frontend-staging-flagsmith.vercel.app
flagsmith-frontend-staging-git-main-flagsmith.vercel.app
staging.flagsmith.com
972f1a3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
flagsmith-frontend-preview – ./frontend
flagsmith-frontend-production-native.vercel.app
flagsmith-frontend-preview-flagsmith.vercel.app
flagsmith-frontend-preview-git-main-flagsmith.vercel.app