addons/isl/src/Bookmark.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ContextMenuItem} from 'shared/ContextMenu';
b69ab319import type {InternalTypes} from './InternalTypes';
b69ab3110import type {CommitInfo} from './types';
b69ab3111
b69ab3112import * as stylex from '@stylexjs/stylex';
b69ab3113import {Button} from 'isl-components/Button';
b69ab3114import {Column} from 'isl-components/Flex';
b69ab3115import {Icon} from 'isl-components/Icon';
b69ab3116import {Tag} from 'isl-components/Tag';
b69ab3117import {TextField} from 'isl-components/TextField';
b69ab3118import {Tooltip} from 'isl-components/Tooltip';
b69ab3119import {useAtomValue} from 'jotai';
b69ab3120import {useState} from 'react';
b69ab3121import {useContextMenu} from 'shared/ContextMenu';
b69ab3122import {spacing} from '../../components/theme/tokens.stylex';
b69ab3123import {tracker} from './analytics';
b69ab3124import {
b69ab3125 bookmarksDataStorage,
b69ab3126 recommendedBookmarksAtom,
b69ab3127 REMOTE_MASTER_BOOKMARK,
b69ab3128} from './BookmarksData';
b69ab3129import {Row} from './ComponentUtils';
b69ab3130import {hiddenMasterFeatureAvailableAtom, shouldHideMasterAtom} from './HiddenMasterData';
b69ab3131import {T, t} from './i18n';
b69ab3132import {Internal} from './Internal';
b69ab3133import {BookmarkCreateOperation} from './operations/BookmarkCreateOperation';
b69ab3134import {BookmarkDeleteOperation} from './operations/BookmarkDeleteOperation';
b69ab3135import {useRunOperation} from './operationsState';
b69ab3136import {latestSuccessorUnlessExplicitlyObsolete} from './successionUtils';
b69ab3137import {showModal} from './useModal';
b69ab3138
b69ab3139const styles = stylex.create({
b69ab3140 stable: {
b69ab3141 backgroundColor: 'var(--list-hover-background)',
b69ab3142 color: 'var(--list-hover-foreground)',
b69ab3143 },
b69ab3144 fullLength: {
b69ab3145 maxWidth: 'unset',
b69ab3146 },
b69ab3147 bookmarkTag: {
b69ab3148 maxWidth: '300px',
b69ab3149 display: 'flex',
b69ab3150 alignItems: 'center',
b69ab3151 gap: spacing.quarter,
b69ab3152 },
b69ab3153 modalButtonBar: {
b69ab3154 justifyContent: 'flex-end',
b69ab3155 },
b69ab3156});
b69ab3157
b69ab3158export type BookmarkKind = 'remote' | 'local' | 'stable';
b69ab3159
b69ab3160function useShouldShowBookmark(): (bookmarkValue: string) => boolean {
b69ab3161 const bookmarksData = useAtomValue(bookmarksDataStorage);
b69ab3162 const shouldHideMaster = useAtomValue(shouldHideMasterAtom);
b69ab3163 const hiddenMasterFeatureAvailable = useAtomValue(hiddenMasterFeatureAvailableAtom);
b69ab3164 return (bookmarkValue: string): boolean => {
b69ab3165 if (bookmarkValue === REMOTE_MASTER_BOOKMARK && hiddenMasterFeatureAvailable) {
b69ab3166 const visibility = bookmarksData.masterBookmarkVisibility;
b69ab3167 if (visibility === 'show') {
b69ab3168 return true;
b69ab3169 }
b69ab3170 if (visibility === 'hide') {
b69ab3171 return false;
b69ab3172 }
b69ab3173 // visibility === 'auto' or undefined - use sitevar config
b69ab3174 return !shouldHideMaster;
b69ab3175 }
b69ab3176 return !bookmarksData.hiddenRemoteBookmarks.includes(bookmarkValue);
b69ab3177 };
b69ab3178}
b69ab3179
b69ab3180const logged = new Set<string>();
b69ab3181function logExposureOncePerSession(location: string) {
b69ab3182 if (logged.has(location)) {
b69ab3183 return;
b69ab3184 }
b69ab3185 tracker.track('SawStableLocation', {extras: {location}});
b69ab3186 logged.add(location);
b69ab3187}
b69ab3188
b69ab3189export function getBookmarkAddons(
b69ab3190 name: string,
b69ab3191 showRecommendedIcon: boolean,
b69ab3192 showWarningOnMaster: boolean,
b69ab3193 tooltipOverride?: string,
b69ab3194): {icon: string | undefined; tooltip: React.ReactNode | undefined} {
b69ab3195 if (showRecommendedIcon) {
b69ab3196 return {icon: 'star-full', tooltip: tooltipOverride ?? Internal.RecommendedBookmarkInfo?.()};
b69ab3197 }
b69ab3198 if (showWarningOnMaster && name === REMOTE_MASTER_BOOKMARK) {
b69ab3199 return {icon: 'warning', tooltip: tooltipOverride ?? Internal.MasterBookmarkInfo?.()};
b69ab31100 }
b69ab31101 return {icon: undefined, tooltip: tooltipOverride};
b69ab31102}
b69ab31103
b69ab31104export function Bookmark({
b69ab31105 children,
b69ab31106 kind,
b69ab31107 fullLength,
b69ab31108 tooltip,
b69ab31109 icon,
b69ab31110}: {
b69ab31111 children: string;
b69ab31112 kind: BookmarkKind;
b69ab31113 fullLength?: boolean;
b69ab31114 tooltip?: string | React.ReactNode;
b69ab31115 icon?: string;
b69ab31116}) {
b69ab31117 const bookmark = children;
b69ab31118 const contextMenu = useContextMenu(makeBookmarkContextMenuOptions);
b69ab31119 const runOperation = useRunOperation();
b69ab31120
b69ab31121 function makeBookmarkContextMenuOptions() {
b69ab31122 const items: Array<ContextMenuItem> = [];
b69ab31123 if (kind === 'local') {
b69ab31124 items.push({
b69ab31125 label: <T replace={{$book: bookmark}}>Delete Bookmark "$book"</T>,
b69ab31126 onClick: () => {
b69ab31127 runOperation(new BookmarkDeleteOperation(bookmark));
b69ab31128 },
b69ab31129 });
b69ab31130 }
b69ab31131 return items;
b69ab31132 }
b69ab31133
b69ab31134 if (kind === 'stable') {
b69ab31135 logExposureOncePerSession(bookmark);
b69ab31136 }
b69ab31137
b69ab31138 const inner = (
b69ab31139 <Tag
b69ab31140 onContextMenu={contextMenu}
b69ab31141 xstyle={[
b69ab31142 kind === 'stable' && styles.stable,
b69ab31143 styles.bookmarkTag,
b69ab31144 fullLength === true && styles.fullLength,
b69ab31145 ]}>
b69ab31146 {icon && <Icon icon={icon} size="XS" style={{display: 'flex', height: '12px'}} />}
b69ab31147 {bookmark}
b69ab31148 </Tag>
b69ab31149 );
b69ab31150 return tooltip ? <Tooltip title={tooltip}>{inner}</Tooltip> : inner;
b69ab31151}
b69ab31152
b69ab31153export function AllBookmarksTruncated({
b69ab31154 stable,
b69ab31155 remote,
b69ab31156 local,
b69ab31157 fullRepoBranch,
b69ab31158}: {
b69ab31159 stable: ReadonlyArray<string | {value: string; description: string; isRecommended?: boolean}>;
b69ab31160 remote: ReadonlyArray<string>;
b69ab31161 local: ReadonlyArray<string>;
b69ab31162 fullRepoBranch?: InternalTypes['FullRepoBranch'] | undefined;
b69ab31163}) {
b69ab31164 const recommendedBookmarks = useAtomValue(recommendedBookmarksAtom);
b69ab31165 const showWarningOnMaster = Internal.shouldCheckRebase?.() ?? false;
b69ab31166 const shouldShowBookmark = useShouldShowBookmark();
b69ab31167
b69ab31168 const FullRepoBranchBookmark = Internal.FullRepoBranchBookmark;
b69ab31169 const compareFullRepoBranch = Internal.compareFullRepoBranch;
b69ab31170
b69ab31171 const finalBookmarks = (
b69ab31172 [
b69ab31173 ['local', local],
b69ab31174 ['remote', remote],
b69ab31175 ['stable', stable],
b69ab31176 ] as const
b69ab31177 )
b69ab31178 .map(([kind, bookmarks]) =>
b69ab31179 bookmarks
b69ab31180 .filter(bookmark =>
b69ab31181 shouldShowBookmark(typeof bookmark === 'string' ? bookmark : bookmark.value),
b69ab31182 )
b69ab31183 .filter(bookmark =>
b69ab31184 compareFullRepoBranch ? compareFullRepoBranch(fullRepoBranch, bookmark) : true,
b69ab31185 )
b69ab31186 .map(bookmark => {
b69ab31187 const value = typeof bookmark === 'string' ? bookmark : bookmark.value;
b69ab31188 const isRecommended =
b69ab31189 recommendedBookmarks.has(value) ||
b69ab31190 (typeof bookmark === 'object' && bookmark.isRecommended === true);
b69ab31191 const tooltipOverride = typeof bookmark === 'string' ? undefined : bookmark.description;
b69ab31192 const {icon, tooltip} = getBookmarkAddons(
b69ab31193 value,
b69ab31194 isRecommended,
b69ab31195 showWarningOnMaster,
b69ab31196 tooltipOverride,
b69ab31197 );
b69ab31198
b69ab31199 return {value, kind, tooltip, icon};
b69ab31200 }),
b69ab31201 )
b69ab31202 .flat();
b69ab31203 const NUM_TO_SHOW = fullRepoBranch == null ? 3 : 2;
b69ab31204 const shownBookmarks = finalBookmarks.slice(0, NUM_TO_SHOW);
b69ab31205 const hiddenBookmarks = finalBookmarks.slice(NUM_TO_SHOW);
b69ab31206 const numTruncated = hiddenBookmarks.length;
b69ab31207
b69ab31208 return (
b69ab31209 <>
b69ab31210 {fullRepoBranch && FullRepoBranchBookmark && (
b69ab31211 <FullRepoBranchBookmark branch={fullRepoBranch} />
b69ab31212 )}
b69ab31213 {shownBookmarks.map(({value, kind, tooltip, icon}) => (
b69ab31214 <Bookmark key={value} kind={kind} tooltip={tooltip} icon={icon}>
b69ab31215 {value}
b69ab31216 </Bookmark>
b69ab31217 ))}
b69ab31218 {numTruncated > 0 && (
b69ab31219 <Tooltip
b69ab31220 component={() => (
b69ab31221 <Column alignStart>
b69ab31222 {hiddenBookmarks.map(({value, kind, tooltip, icon}) => (
b69ab31223 <Bookmark key={value} kind={kind} tooltip={tooltip} icon={icon} fullLength>
b69ab31224 {value}
b69ab31225 </Bookmark>
b69ab31226 ))}
b69ab31227 </Column>
b69ab31228 )}>
b69ab31229 <Tag>
b69ab31230 <T replace={{$n: numTruncated}}>+$n more</T>
b69ab31231 </Tag>
b69ab31232 </Tooltip>
b69ab31233 )}
b69ab31234 </>
b69ab31235 );
b69ab31236}
b69ab31237
b69ab31238export function Bookmarks({
b69ab31239 bookmarks,
b69ab31240 kind,
b69ab31241}: {
b69ab31242 bookmarks: ReadonlyArray<string | {value: string; description: string}>;
b69ab31243 kind: BookmarkKind;
b69ab31244}) {
b69ab31245 const shouldShowBookmark = useShouldShowBookmark();
b69ab31246
b69ab31247 return (
b69ab31248 <>
b69ab31249 {bookmarks
b69ab31250 .filter(bookmark =>
b69ab31251 shouldShowBookmark(typeof bookmark === 'string' ? bookmark : bookmark.value),
b69ab31252 )
b69ab31253 .map(bookmark => {
b69ab31254 const value = typeof bookmark === 'string' ? bookmark : bookmark.value;
b69ab31255 const tooltip = typeof bookmark === 'string' ? undefined : bookmark.description;
b69ab31256 return (
b69ab31257 <Bookmark key={value} kind={kind} tooltip={tooltip}>
b69ab31258 {value}
b69ab31259 </Bookmark>
b69ab31260 );
b69ab31261 })}
b69ab31262 </>
b69ab31263 );
b69ab31264}
b69ab31265
b69ab31266export async function createBookmarkAtCommit(commit: CommitInfo) {
b69ab31267 await showModal({
b69ab31268 type: 'custom',
b69ab31269 title: <T>Create Bookmark</T>,
b69ab31270 component: ({returnResultAndDismiss}: {returnResultAndDismiss: (data?: undefined) => void}) => (
b69ab31271 <CreateBookmarkAtCommitModal commit={commit} dismiss={returnResultAndDismiss} />
b69ab31272 ),
b69ab31273 });
b69ab31274}
b69ab31275
b69ab31276function CreateBookmarkAtCommitModal({commit, dismiss}: {commit: CommitInfo; dismiss: () => void}) {
b69ab31277 const runOperation = useRunOperation();
b69ab31278 const [bookmark, setBookmark] = useState('');
b69ab31279 return (
b69ab31280 <>
b69ab31281 <TextField
b69ab31282 autoFocus
b69ab31283 value={bookmark}
b69ab31284 onChange={e => setBookmark(e.currentTarget.value)}
b69ab31285 aria-label={t('Bookmark Name')}
b69ab31286 />
b69ab31287 <Row {...stylex.props(styles.modalButtonBar)}>
b69ab31288 <Button
b69ab31289 onClick={() => {
b69ab31290 dismiss();
b69ab31291 }}>
b69ab31292 <T>Cancel</T>
b69ab31293 </Button>
b69ab31294 <Button
b69ab31295 primary
b69ab31296 onClick={() => {
b69ab31297 runOperation(
b69ab31298 new BookmarkCreateOperation(
b69ab31299 latestSuccessorUnlessExplicitlyObsolete(commit),
b69ab31300 bookmark,
b69ab31301 ),
b69ab31302 );
b69ab31303 dismiss();
b69ab31304 }}
b69ab31305 disabled={bookmark.trim().length === 0}>
b69ab31306 <T>Create</T>
b69ab31307 </Button>
b69ab31308 </Row>
b69ab31309 </>
b69ab31310 );
b69ab31311}