diff --git a/external/@worldbrain/memex-common b/external/@worldbrain/memex-common index f9c9cfeca4..0cec80d34d 160000 --- a/external/@worldbrain/memex-common +++ b/external/@worldbrain/memex-common @@ -1 +1 @@ -Subproject commit f9c9cfeca480911abeeca0b8cb4c626d944d7f8f +Subproject commit 0cec80d34d423f137e02e3cfc7e0d238927cbd1b diff --git a/src/annotations/components/AnnotationEditable.tsx b/src/annotations/components/AnnotationEditable.tsx index dbddd04595..e75ce528c8 100644 --- a/src/annotations/components/AnnotationEditable.tsx +++ b/src/annotations/components/AnnotationEditable.tsx @@ -42,6 +42,7 @@ import CreationInfo from '@worldbrain/memex-common/lib/common-ui/components/crea import Checkbox from 'src/common-ui/components/Checkbox' import KeyboardShortcuts from '@worldbrain/memex-common/lib/common-ui/components/keyboard-shortcuts' import type { HighlightColor } from '@worldbrain/memex-common/lib/common-ui/components/highlightColorPicker/types' +import CheckboxNotInput from 'src/common-ui/components/CheckboxNotInput' export interface HighlightProps extends AnnotationProps { body: string @@ -53,6 +54,7 @@ export interface HighlightProps extends AnnotationProps { export interface NoteProps extends AnnotationProps { body?: string comment: string + isBulkSelected: boolean } export interface AnnotationProps { @@ -127,6 +129,8 @@ export interface AnnotationProps { getRootElement: () => HTMLElement toggleAutoAdd: () => void isAutoAddEnabled?: boolean + bulkSelectAnnotation: () => void + isBulkSelected: boolean } export interface AnnotationEditableEventProps { @@ -1184,71 +1188,110 @@ export default class AnnotationEditable extends React.Component { ) } + private renderBulkSelectBtn(): JSX.Element { + return ( + + Multi Select Items +
+ {AnnotationEditable.MOD_KEY}+Ato select + all + + } + placement="bottom" + getPortalRoot={this.props.getRootElement} + > + + , + ) => { + this.props.bulkSelectAnnotation() + event.stopPropagation() + }} + size={16} + /> + +
+ ) + } + render() { const { annotationFooterDependencies } = this.props const { annotationFooterDependencies: footerDeps, onGoToAnnotation, + bulkSelectAnnotation, } = this.props - const actionsBox = - !this.props.isEditingHighlight && this.state.hoverCard ? ( - - {footerDeps.onDeleteIconClick && ( - - - - )} - {onGoToAnnotation && ( - - - - )} - {footerDeps?.onEditIconClick && - this.props.currentUserId === this.props.creatorId ? ( - - Add/Edit Note -
- or double-click card - - } - placement="bottom" - getPortalRoot={this.props.getRootElement} - > - -
- ) : undefined} -
- ) : null + const actionsBox = !this.props.isEditingHighlight ? ( + + {this.state.hoverCard && ( + <> + {footerDeps.onDeleteIconClick && ( + + + + )} + {onGoToAnnotation && ( + + + + )} + {footerDeps?.onEditIconClick && + this.props.currentUserId === this.props.creatorId ? ( + + Add/Edit Note +
+ or double-click card + + } + placement="bottom" + getPortalRoot={this.props.getRootElement} + > + +
+ ) : undefined} + + )} + {bulkSelectAnnotation && + (this.props.isBulkSelected || this.state.hoverCard) && + this.props.currentUserId === this.props.creatorId && + bulkSelectAnnotation && + this.renderBulkSelectBtn()} +
+ ) : null return ( @@ -1751,3 +1794,11 @@ const TooltipTextBox = styled.div` const CreationInfoBox = styled.div` padding: 15px 15px 0px 15px; ` + +const BulkSelectButtonBox = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; +` diff --git a/src/dashboard-refactor/search-results/components/list-details.tsx b/src/dashboard-refactor/search-results/components/list-details.tsx index c84fcfb237..1ebd74042e 100644 --- a/src/dashboard-refactor/search-results/components/list-details.tsx +++ b/src/dashboard-refactor/search-results/components/list-details.tsx @@ -687,6 +687,14 @@ const DescriptionContainer = styled.div` margin-top: 10px; display: flex; justify-content: flex-start; + overflow: scroll; + max-height: 70vh; + + &::-webkit-scrollbar { + display: none; + } + + scrollbar-width: none; &:hover ${DescriptionEditContainer} { display: flex; diff --git a/src/dashboard-refactor/search-results/index.tsx b/src/dashboard-refactor/search-results/index.tsx index 3b29f5a8c6..6bf298abac 100644 --- a/src/dashboard-refactor/search-results/index.tsx +++ b/src/dashboard-refactor/search-results/index.tsx @@ -1237,7 +1237,7 @@ export default class SearchResultsContainer extends React.Component< } const ResultsScrollContainer = styled.div` - overflow-y: scroll; + overflow-y: hidden; overflow-x: hidden; height: fill-available; width: fill-available; @@ -1598,6 +1598,7 @@ const ResultsContainer = styled.div` flex: 1; height: fill-available; height: -moz-available; + overflow: scroll; &::-webkit-scrollbar { display: none; diff --git a/src/overview/search-bar/components/DateRangeSelection.tsx b/src/overview/search-bar/components/DateRangeSelection.tsx index a7a54d95fe..be88176a66 100644 --- a/src/overview/search-bar/components/DateRangeSelection.tsx +++ b/src/overview/search-bar/components/DateRangeSelection.tsx @@ -43,7 +43,6 @@ class DateRangeSelection extends Component { } componentDidMount() { - console.log('DateRangeSelection mounted', arrowDown) // Override clear button handlers this.startDatePicker.onClearClick = this.handleClearClick({ isStartDate: true, diff --git a/src/sidebar/annotations-sidebar/components/AnnotationsSidebar.tsx b/src/sidebar/annotations-sidebar/components/AnnotationsSidebar.tsx index dd209befcb..4690dddc3c 100644 --- a/src/sidebar/annotations-sidebar/components/AnnotationsSidebar.tsx +++ b/src/sidebar/annotations-sidebar/components/AnnotationsSidebar.tsx @@ -102,6 +102,8 @@ import { AnalyticsCoreInterface } from '@worldbrain/memex-common/lib/analytics/t import PageCitations from 'src/citations/PageCitations' import { TaskState } from 'ui-logic-core/lib/types' import TutorialBox from '@worldbrain/memex-common/lib/common-ui/components/tutorial-box' +import SpacePicker from 'src/custom-lists/ui/CollectionPicker' +import debounce from 'lodash/debounce' const SHOW_ISOLATED_VIEW_KEY = `show-isolated-view-notif` @@ -156,6 +158,7 @@ export interface AnnotationsSidebarProps extends SidebarContainerState { closePicker?: () => void, referenceElement?: React.RefObject, ) => JSX.Element + renderListPickerForBulkEdit: () => JSX.Element renderContextMenuForList: (listData: UnifiedList) => JSX.Element renderEditMenuForList: (listData: UnifiedList) => JSX.Element renderPageLinkMenuForList: () => JSX.Element @@ -292,6 +295,9 @@ export interface AnnotationsSidebarProps extends SidebarContainerState { showSpacesTab: () => void isAutoAddEnabled: boolean toggleAutoAdd: () => void + toggleAutoAddBulk: (toggleState: boolean) => void + bulkSelectAnnotations: (annotationIds: string[]) => void + bulkSelectionState: string[] } interface AnnotationsSidebarState { @@ -319,6 +325,8 @@ interface AnnotationsSidebarState { fileDragOverFeedField?: boolean showSelectedAITextButtons?: boolean pageLinkCreationLoading: TaskState + showSpacePickerForBulkEdit: boolean + showAutoAddBulkSelection: boolean } export class AnnotationsSidebar extends React.Component< @@ -332,7 +340,8 @@ export class AnnotationsSidebar extends React.Component< private sortDropDownButtonRef = React.createRef() private copyButtonRef = React.createRef() private pageSummaryText = React.createRef() - private pageShareButtonRef = React.createRef() + private autoAddBulkButtonRef = React.createRef() + private addSpaceBulkButtonRef = React.createRef() private bulkEditButtonRef = React.createRef() private editPageLinkButtonRef = React.createRef() private sharePageLinkButtonRef = React.createRef() @@ -349,6 +358,7 @@ export class AnnotationsSidebar extends React.Component< [unifiedListId: string]: React.RefObject } = {} private editorPassedUp = false + lastClickInsideSidebar = null state: AnnotationsSidebarState = { searchText: '', @@ -371,6 +381,8 @@ export class AnnotationsSidebar extends React.Component< fileDragOverFeedField: false, showSelectedAITextButtons: false, pageLinkCreationLoading: 'pristine', + showSpacePickerForBulkEdit: false, + showAutoAddBulkSelection: false, } async addYoutubeTimestampToEditor(commentText) { @@ -444,6 +456,10 @@ export class AnnotationsSidebar extends React.Component< } this.setState({ themeVariant }) this.props.getHighlightColorSettings() + + document.addEventListener('keydown', this.handleSelectAll) + document.addEventListener('mousedown', this.handleLastClick) + document.addEventListener('mousemove', this.trackMouseOverSidebar) } async componentDidUpdate( @@ -456,7 +472,71 @@ export class AnnotationsSidebar extends React.Component< } } - componentWillUnmount() {} + componentWillUnmount(): void { + document.removeEventListener('keydown', this.handleSelectAll) + document.removeEventListener('mousedown', this.handleLastClick) + document.removeEventListener('mousemove', this.trackMouseOverSidebar) + } + + handleLastClick = (e) => { + const rootElement = this.props.getRootElement() + const sidebarContainer = rootElement.querySelector( + '#annotationSidebarContainer', + ) + if (sidebarContainer && e.composedPath().includes(sidebarContainer)) { + this.lastClickInsideSidebar = true + } else { + this.lastClickInsideSidebar = false + } + } + + trackMouseOverSidebar = debounce((e: MouseEvent) => { + const rootElement = this.props.getRootElement() + const sidebarContainer = rootElement.querySelector( + '#annotationSidebarContainer', + ) + + if (!sidebarContainer) return + + const { + left, + top, + width, + height, + } = sidebarContainer.getBoundingClientRect() + const isMouseOverSidebar = + e.clientX >= left && + e.clientX <= left + width && + e.clientY >= top && + e.clientY <= top + height + + if (isMouseOverSidebar) { + this.lastClickInsideSidebar = true + } else { + this.lastClickInsideSidebar = false + } + }, 100) + + handleSelectAll = async (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'a') { + e.preventDefault() + + if (this.lastClickInsideSidebar) { + const annotations = cacheUtils.getUserAnnotationsArray( + { + annotations: this.props.annotations, + }, + this.props.normalizedPageUrl, + this.props.currentUser?.id.toString(), + ) + + const annotationIds = annotations.map((annotation) => { + return annotation.unifiedId + }) + this.props.bulkSelectAnnotations(annotationIds) + } + } + } handleClickOutside = (event) => { this.spaceTitleEditFieldRef.current.removeEventListener( @@ -862,6 +942,14 @@ export class AnnotationsSidebar extends React.Component< color, ) } + bulkSelectAnnotation={() => + this.props.bulkSelectAnnotations([ + annotation.unifiedId, + ]) + } + isBulkSelected={this.props.bulkSelectionState.includes( + annotation.unifiedId, + )} saveHighlightColorSettings={ this.props.saveHighlightColorSettings } @@ -1073,6 +1161,8 @@ export class AnnotationsSidebar extends React.Component< }} /> + {this.props.bulkSelectionState?.length > 0 && + this.renderBulkEditBar()} )} {listAnnotations} @@ -3732,6 +3822,14 @@ export class AnnotationsSidebar extends React.Component< isEditingHighlight={ instanceState.isHighlightEditing } + bulkSelectAnnotation={() => + this.props.bulkSelectAnnotations([ + annot.unifiedId, + ]) + } + isBulkSelected={this.props.bulkSelectionState.includes( + annot.unifiedId, + )} isDeleting={ instanceState.cardMode === 'delete-confirm' } @@ -3835,6 +3933,8 @@ export class AnnotationsSidebar extends React.Component< {this.renderTopBarActionButtons()} )} + {this.props.bulkSelectionState?.length > 0 && + this.renderBulkEditBar()} {this.props.noteCreateState === 'running' || annotations?.length > 0 ? ( @@ -4500,7 +4600,6 @@ export class AnnotationsSidebar extends React.Component< } return ( this.setState({ @@ -4514,6 +4613,112 @@ export class AnnotationsSidebar extends React.Component< ) } + renderBulkSpacePicker() { + if (!this.state.showSpacePickerForBulkEdit) { + return + } + return ( + + this.setState({ + showSpacePickerForBulkEdit: false, + }) + } + offsetX={10} + getPortalRoot={this.props.getRootElement} + > + {this.props.renderListPickerForBulkEdit()} + + ) + } + renderAutoAddBulkSelection() { + if (!this.state.showAutoAddBulkSelection) { + return + } + return ( + + this.setState({ + showAutoAddBulkSelection: false, + }) + } + offsetX={10} + getPortalRoot={this.props.getRootElement} + > + + { + this.setState({ + showAutoAddBulkSelection: false, + }) + this.props.toggleAutoAddBulk(true) + }} + > + Enable Auto Added + + + { + this.setState({ + showAutoAddBulkSelection: false, + }) + this.props.toggleAutoAddBulk(false) + }} + > + Disable Auto Added + + + + ) + } + + renderBulkEditBar() { + return ( + + this.props.bulkSelectAnnotations([])} + label={this.props.bulkSelectionState?.length + ' Selected'} + type={'tertiary'} + size={'small'} + height={'30px'} + icon={'removeX'} + fontColor="greyScale7" + iconPosition="right" + /> + + + + this.setState({ showSpacePickerForBulkEdit: true }) + } + label={'Spaces'} + type={'tertiary'} + size={'small'} + height={'30px'} + icon={'plus'} + innerRef={this.addSpaceBulkButtonRef} + /> + + this.setState({ showAutoAddBulkSelection: true }) + } + label={'Change Status'} + type={'tertiary'} + size={'small'} + height={'30px'} + icon={'spread'} + innerRef={this.autoAddBulkButtonRef} + /> + + {this.renderAutoAddBulkSelection()} + {this.renderBulkSpacePicker()} + + ) + } render() { if (!this.state.themeVariant) { @@ -6255,3 +6460,52 @@ const TutorialButtonContainer = styled.div` right: 20px; bottom: 15px; ` +const BulkEditBarContainer = styled.div` + display: flex; + align-items: center; + padding: 0 15px 0 15px; + height: 40px; + border-top: 1px solid ${(props) => props.theme.colors.greyScale2}; + border-bottom: 1px solid ${(props) => props.theme.colors.greyScale2}; + color: ${(props) => props.theme.colors.greyScale6}; + font-size: 14px; + margin-bottom: 10px; + width: 100%; + width: fill-available; + justify-content: space-between; + grid-gap: 10px; +` + +const BulkEditBarActionBar = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + grid-gap: 10px; +` + +const AutoAddBulkSelectionContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + grid-gap: 5px; + padding: 10px; + width: fit-content; +` + +const AutoAddBulkSelection = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + grid-gap: 5px; + padding: 0 15px; + height: 40px; + width: fit-conten; + border-radius: 5px; + &:hover { + cursor: pointer; + background: ${(props) => props.theme.colors.greyScale2}; + } + color: ${(props) => props.theme.colors.greyScale6}; + font-size: 14px; +` diff --git a/src/sidebar/annotations-sidebar/containers/AnnotationsSidebarContainer.tsx b/src/sidebar/annotations-sidebar/containers/AnnotationsSidebarContainer.tsx index 038e2e90c2..d4a444f091 100644 --- a/src/sidebar/annotations-sidebar/containers/AnnotationsSidebarContainer.tsx +++ b/src/sidebar/annotations-sidebar/containers/AnnotationsSidebarContainer.tsx @@ -544,6 +544,44 @@ export class AnnotationsSidebarContainer< } } + getSpacePickerPropsForBulkEdit = () => { + const { + authBG, + customListsBG, + contentSharingBG, + annotationsCache, + pageActivityIndicatorBG, + } = this.props + // This is to show confirmation modal if the annotation is public and the user is trying to add it to a shared space + + return { + authBG, + annotationsCache, + contentSharingBG, + analyticsBG: this.props.analyticsBG, + pageActivityIndicatorBG, + spacesBG: customListsBG, + showPageLinks: true, + bgScriptBG: this.props.bgScriptBG, + localStorageAPI: this.props.storageAPI.local, + initialSelectedListIds: () => null, + selectEntry: async (listId, options) => { + this.processEvent('updateListsForAnnotationS', { + added: listId, + deleted: null, + }) + }, + unselectEntry: async (listId) => + this.processEvent('updateListsForAnnotationS', { + added: null, + deleted: listId, + }), + normalizedPageUrlToFilterPageLinksBy: normalizeUrl( + this.state.fullPageUrl, + ), + } + } + private renderCopyPasterManagerForAnnotation = ( instanceLocation: AnnotationCardInstanceLocation, ) => (unifiedId: UnifiedAnnotation['unifiedId']) => { @@ -625,6 +663,42 @@ export class AnnotationsSidebarContainer< /> ) } + private renderListPickerForBulkEdit = () => { + return ( + { + this.processEvent('updateListsForAnnotationS', { + added: listId, + deleted: null, + }) + }} + unselectEntry={async (listId) => + this.processEvent('updateListsForAnnotationS', { + added: null, + deleted: listId, + }) + } + onListFocus={(listId: UnifiedList['localId']) => { + const unifiedListId: UnifiedList['unifiedId'] = this.props.annotationsCache.getListByLocalId( + listId, + ).unifiedId + + this.processEvent('setSelectedList', { unifiedListId }) + }} + getRootElement={this.props.getRootElement} + autoFocus + /> + ) + } private getRemoteIdsForCacheIds = (listIds: string[]): string[] => listIds @@ -1546,6 +1620,9 @@ export class AnnotationsSidebarContainer< renderListsPickerForAnnotation={ this.renderListPickerForAnnotation } + renderListPickerForBulkEdit={ + this.renderListPickerForBulkEdit + } setActiveTab={(tab) => (event) => { this.processEvent('setActiveSidebarTab', { tab, @@ -1676,7 +1753,19 @@ export class AnnotationsSidebarContainer< toggleAutoAdd={() => this.processEvent('toggleAutoAdd', null) } + toggleAutoAddBulk={(toggleState) => { + this.processEvent('toggleAutoAddBulk', { + shouldAutoAdd: toggleState, + }) + }} isAutoAddEnabled={this.state.isAutoAddEnabled} + bulkSelectAnnotations={( + annotationIds: UnifiedAnnotation['unifiedId'][], + ) => { + this.processEvent('bulkSelectAnnotations', { + annotationIds: annotationIds, + }) + }} /> @@ -1760,7 +1849,7 @@ const ContainerStyled = styled.div<{ : '2147483645'}; /* This is to combat pages setting high values on certain elements under the sidebar */ background: ${(props) => props.theme.variant === 'dark' - ? '#23242b' + ? props.theme.colors.black0 : props.theme.colors.black + 'c9'}; border-left: 1px solid ${(props) => props.theme.colors.greyScale2}; font-family: 'Satoshi', sans-serif; @@ -1816,7 +1905,7 @@ const ContainerStyled = styled.div<{ css` background: ${(props) => props.theme.variant === 'dark' - ? '#23242b' + ? props.theme.colors.black0 : props.theme.colors.black + 'c9'}; backdrop-filter: unset; position: relative; diff --git a/src/sidebar/annotations-sidebar/containers/logic.ts b/src/sidebar/annotations-sidebar/containers/logic.ts index f7e64e39b3..47a8c7b9e5 100644 --- a/src/sidebar/annotations-sidebar/containers/logic.ts +++ b/src/sidebar/annotations-sidebar/containers/logic.ts @@ -376,6 +376,7 @@ export class SidebarContainerLogic extends UILogic< showFeedSourcesMenu: false, existingSourcesOption: 'pristine', localFoldersList: [], + bulkSelectionState: [], } } @@ -2105,6 +2106,80 @@ export class SidebarContainerLogic extends UILogic< }) } + updateListsForAnnotationS: EventHandler< + 'updateListsForAnnotationS' + > = async ({ event, previousState }) => { + const annotationIds = previousState.bulkSelectionState + + for (let annotationId of annotationIds) { + this.processUIEvent('updateListsForAnnotation', { + event: { + added: event.added, + deleted: event.deleted, + unifiedAnnotationId: annotationId, + }, + previousState, + }) + } + } + + toggleAutoAddBulk: EventHandler<'toggleAutoAddBulk'> = async ({ + event, + previousState, + }) => { + const annotationIds = previousState.bulkSelectionState + const shouldAutoAdd = event.shouldAutoAdd + + for (let annotationId of annotationIds) { + this.processUIEvent('editAnnotation', { + event: { + unifiedAnnotationId: annotationId, + instanceLocation: 'annotations-tab', + shouldShare: shouldAutoAdd, + isProtected: !shouldAutoAdd, + mainBtnPressed: null, + keepListsIfUnsharing: true, + now: Date.now(), + }, + previousState, + }) + } + } + + bulkSelectAnnotations: EventHandler<'bulkSelectAnnotations'> = async ({ + event, + previousState, + }) => { + const annotationIds = event.annotationIds + + if (annotationIds.length === 0) { + this.emitMutation({ + bulkSelectionState: { $set: [] }, + }) + return + } + + let bulkSelectionArray = previousState.bulkSelectionState ?? [] + + for (let annotationId of annotationIds) { + const index = bulkSelectionArray.indexOf(annotationId) + if (index !== -1) { + bulkSelectionArray.splice(index, 1) + this.emitMutation({ + bulkSelectionState: { $set: bulkSelectionArray }, + }) + } else { + bulkSelectionArray.push(annotationId) + this.emitMutation({ + bulkSelectionState: { $set: bulkSelectionArray }, + }) + } + this.emitMutation({ + bulkSelectionState: { $set: bulkSelectionArray }, + }) + } + } + setAnnotationEditCommentText: EventHandler< 'setAnnotationEditCommentText' > = async ({ event }) => { diff --git a/src/sidebar/annotations-sidebar/containers/types.ts b/src/sidebar/annotations-sidebar/containers/types.ts index 239b6a2a38..9d29bf82a2 100644 --- a/src/sidebar/annotations-sidebar/containers/types.ts +++ b/src/sidebar/annotations-sidebar/containers/types.ts @@ -294,6 +294,7 @@ export interface SidebarContainerState extends AnnotationConversationsState { AImodel: 'gpt-3.5-turbo-1106' | 'gpt-4-0613' | 'gpt-4-32k' localFoldersList: LocalFolder[] showFeedSourcesMenu: boolean + bulkSelectionState: string[] } export interface LocalFolder { @@ -430,6 +431,11 @@ interface SidebarEvents { // List instance events expandListAnnotations: { unifiedListId: UnifiedList['unifiedId'] } markFeedAsRead: null + bulkSelectAnnotations: { + annotationIds: UnifiedAnnotation['unifiedId'][] + } + + toggleAutoAddBulk: { shouldAutoAdd: boolean } // Annotation card instance events setAnnotationEditCommentText: AnnotationCardInstanceEvent<{ @@ -474,6 +480,10 @@ interface SidebarEvents { deleted: number | null options?: { protectAnnotation?: boolean } }> + updateListsForAnnotationS: { + added: number | null + deleted: number | null + } updateAnnotationShareInfo: AnnotationEvent<{ keepListsIfUnsharing?: boolean privacyLevel: AnnotationPrivacyLevels @@ -592,6 +602,7 @@ export interface AnnotationCardInstance { body: string color: string copyLoadingState: TaskState + isBulkSelected: boolean } export interface SuggestionCard { fullUrl: UnifiedAnnotation['unifiedId']