Skip to content

Commit

Permalink
Add support for displaying trashed collections
Browse files Browse the repository at this point in the history
  • Loading branch information
tnajdek committed Sep 11, 2024
1 parent 4fddcbc commit e8138ba
Show file tree
Hide file tree
Showing 29 changed files with 286 additions and 151 deletions.
9 changes: 6 additions & 3 deletions src/js/actions/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ const fetchAllCollections = (libraryKey, { sort = 'dateModified', direction = "d
const state = getState();
const expectedCount = get(state, ['libraries', libraryKey, 'collections', 'totalResults'], null);
const isKnown = expectedCount !== null;
const collections = get(state, ['libraries', libraryKey, 'collections', 'data'], {});
const collectionKeys = state.libraries[libraryKey]?.collections?.keys ?? [];
const isFetchingAll = get(state, ['libraries', libraryKey, 'collections', 'isFetchingAll'], null);
const actualCount = Object.keys(collections).length;
const actualCount = Object.keys(collectionKeys).length;
const isCountCorrect = expectedCount === actualCount

if(isFetchingAll) {
Expand Down Expand Up @@ -263,7 +263,10 @@ const queueUpdateCollection = (collectionKey, patch, libraryKey, queueId) => {
callback: async (next, dispatch, getState) => {
const state = getState();
const config = state.config;
const collection = get(state, ['libraries', libraryKey, 'collections', 'data', collectionKey]);
const collection = state.libraries[libraryKey]?.dataObjects[collectionKey];
if (!collection) {
throw new Error(`Collection ${collectionKey} not found in library ${libraryKey}`);
}
const version = collection.version;

dispatch({
Expand Down
4 changes: 2 additions & 2 deletions src/js/actions/navigate.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,9 @@ const redirectIfCollectionNotFound = () => {
const state = getState();
const libraryKey = state.current.libraryKey;
const collectionKey = state.current.collectionKey;
const collections = state.libraries[libraryKey]?.collections?.data;
const dataObjects = state.libraries[libraryKey]?.dataObjects;

if (collectionKey !== null && collections !== null && !(collectionKey in collections)) {
if (collectionKey !== null && dataObjects !== null && !(collectionKey in dataObjects)) {
process.env.NODE_ENV === 'development' && console.warn(`Collection ${collectionKey} not found in library ${libraryKey}`);
dispatch(navigate({ library: libraryKey, view: 'library' }, true));
}
Expand Down
1 change: 1 addition & 0 deletions src/js/actions/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const runRequest = async (dispatch, request, { id, type, payload }, requestOpts
error
});
} else {
console.error(error);
dispatch({
type: `ERROR_${type}`,
...payload, id,
Expand Down
34 changes: 26 additions & 8 deletions src/js/common/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ const getBaseMappedValue = (mappings, item, property) => {
item[mappings[itemType][property]] : property in item ? item[property] : null;
}

const getItemTitle = (mappings, item) => item.itemType === 'note' ?
noteAsTitle(item.note) : getBaseMappedValue(mappings, item, 'title') || '';
const getItemTitle = (mappings, item) => {
if (item.itemType === 'note') {
return noteAsTitle(item.note);
}
if (!item.itemType && item.name) {
return item.name;
}
return getBaseMappedValue(mappings, item, 'title') || '';
}

// logic based on:
// https://github.com/zotero/zotero/blob/26ee0e294b604ed9ea473c76bb072715c318eac2/chrome/content/zotero/xpcom/data/item.js#L3697
Expand Down Expand Up @@ -102,9 +109,13 @@ const getDerivedData = (mappings, item, itemTypes, tagColors) => {
const colors = [];
const emojis = [];

if (!tagColors?.value || !tagColors?.lookup) {
tagColors = { value: [], lookup: {} };
}

// colored tags, including emoji tags, ordered by position (value is an array ordered by position)
tagColors.value.forEach(({ name, color }) => {
if(!item.tags.some(({ tag }) => tag === name)) {
if(!(item?.tags ?? []).some(({ tag }) => tag === name)) {
return;
}
if (containsEmoji(name)) {
Expand All @@ -115,7 +126,7 @@ const getDerivedData = (mappings, item, itemTypes, tagColors) => {
});

// non-colored tags containing emoji, sorted alphabetically (item.tags should already be sorted)
item.tags.forEach(({ tag }) => {
(item?.tags ?? []).forEach(({ tag }) => {
if (!(tag in tagColors.lookup) && containsEmoji(tag)) {
emojis.push(extractEmoji(tag));
}
Expand All @@ -125,7 +136,7 @@ const getDerivedData = (mappings, item, itemTypes, tagColors) => {
item[Symbol.for('meta')].createdByUser.username :
'';
const itemTypeName = itemTypeLocalized(item, itemTypes);
const iconName = item.itemType === 'attachment' ? getAttachmentIcon(item) : getItemTypeIcon(item.itemType);
const iconName = (!item.itemType && item.name) ? 'folder' : item.itemType === 'attachment' ? getAttachmentIcon(item) : getItemTypeIcon(item.itemType);


// same logic as https://github.com/zotero/zotero/blob/6abfd3b5b03969564424dc03313d63ae1de86100/chrome/content/zotero/xpcom/itemTreeView.js#L1062
Expand Down Expand Up @@ -157,6 +168,15 @@ const getDerivedData = (mappings, item, itemTypes, tagColors) => {
}
};

const calculateDerivedData = (item, { meta = { mappings: {}, itemTypes: [] }, tagColors } = {}) => {
if (Array.isArray(item)) {
return item.map(i => calculateDerivedData(i, { meta, tagColors }));
}

item[Symbol.for('derived')] = getDerivedData(meta.mappings, item, meta.itemTypes, tagColors);
return item;
}

const getFieldDisplayValue = (item, field) => {
switch(field) {
case 'accessDate':
Expand All @@ -172,6 +192,4 @@ const getFieldDisplayValue = (item, field) => {
const getLastPageIndexSettingKey = (itemKey, libraryKey) =>
`lastPageIndex_${libraryKey[0] === 'u' ? 'u' : libraryKey}_${itemKey}`;

export { getBaseMappedValue, getDerivedData, getFieldDisplayValue,
getItemTitle, getLastPageIndexSettingKey
};
export { calculateDerivedData, getBaseMappedValue, getDerivedData, getFieldDisplayValue, getItemTitle, getLastPageIndexSettingKey };
2 changes: 1 addition & 1 deletion src/js/common/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const getItemKeysPath = ({ itemsSource, libraryKey, collectionKey }) => {
case 'top':
return ['libraries', libraryKey, 'itemsTop'];
case 'trash':
return ['libraries', libraryKey, 'itemsTrash'];
return ['libraries', libraryKey, 'itemsAndCollectionsTrash'];
case 'publications':
return ['itemsPublications'];
case 'collection':
Expand Down
4 changes: 2 additions & 2 deletions src/js/component/embedded/header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const EmbeddedCollectionPicker = props => {
const { tabIndex = -2, onFocusNext, onFocusPrev } = props;
const libraryName = useSelector(state => state.config.libraries?.[0].name);
const collectionKey = useSelector(state => state.current.collectionKey)
const collectionName = useSelector(state => state.libraries[state.current.libraryKey]?.collections?.data?.[state.current.collectionKey]?.name);
const collectionName = useSelector(state => state.libraries[state.current.libraryKey]?.dataObjects?.[state.current.collectionKey]?.name);

const dispatch = useDispatch();

Expand Down Expand Up @@ -83,7 +83,7 @@ const EmbeddedHeader = () => {
const dispatch = useDispatch();
const backLabel = useSelector(state =>
state.current.collectionKey ?
state.libraries[state.current.libraryKey]?.collections?.data?.[state.current.collectionKey]?.name :
state.libraries[state.current.libraryKey]?.dataObjects?.[state.current.collectionKey]?.name :
state.config.libraries?.[0].name
);
const { receiveFocus, receiveBlur, focusNext, focusPrev } = useFocusManager(ref, '.search-input');
Expand Down
20 changes: 12 additions & 8 deletions src/js/component/item/details.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const ItemDetails = props => {
const itemKey = useSelector(state => state.current.itemKey);
const libraryKey = useSelector(state => state.current.libraryKey);
const isEmbedded = useSelector(state => state.config.isEmbedded);
const item = useSelector(state => get(state, ['libraries', libraryKey, 'items', itemKey], null));
const item = useSelector(state => get(state, ['libraries', libraryKey, 'dataObjects', itemKey], null));
// collections are prefetched so if item is null, it's not a collection
const isCollection = item?.[Symbol.for('type')] === 'collection';
const shouldFetchItem = itemKey && !isCollection && !item;
const isSelectMode = useSelector(state => state.current.isSelectMode);
const isTrash = useSelector(state => state.current.isTrash);
const shouldRedirectToParentItem = !isTrash && itemKey && item && item.parentItem;
Expand All @@ -28,10 +31,10 @@ const ItemDetails = props => {
});

useEffect(() => {
if(itemKey && !item) {
if (shouldFetchItem) {
dispatch(fetchItemDetails(itemKey));
}
}, [dispatch, item, itemKey]);
}, [dispatch, itemKey, shouldFetchItem]);

useEffect(() => {
if(lastFetchItemDetailsNoResults) {
Expand All @@ -57,11 +60,12 @@ const ItemDetails = props => {
/>
) }
{
(!isTouchOrSmall || (isTouchOrSmall && !isSelectMode))
&& item && !shouldRedirectToParentItem ? (
<ItemDetailsTabs />
) : (
<ItemDetailsInfoView />
isCollection ? null : (
(!isTouchOrSmall || (isTouchOrSmall && !isSelectMode)) && item && !shouldRedirectToParentItem ? (
<ItemDetailsTabs />
) : (
<ItemDetailsInfoView />
)
)
}
</section>
Expand Down
12 changes: 9 additions & 3 deletions src/js/component/item/items/list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const ItemsList = memo(props => {
const listRef = useRef(null);
const lastRequest = useRef({});
const dispatch = useDispatch();
const { hasChecked, isFetching, keys, requests, totalResults, sortBy, sortDirection } = useSourceData();
const { injectPoints, hasChecked, isFetching, keys, requests, totalResults, sortBy, sortDirection } = useSourceData();
const prevSortBy = usePrevious(sortBy);
const prevSortDirection = usePrevious(sortDirection);
const isSearchMode = useSelector(state => state.current.isSearchMode);
Expand Down Expand Up @@ -51,9 +51,15 @@ const ItemsList = memo(props => {
}, [keys, requests]);

const handleLoadMore = useCallback((startIndex, stopIndex) => {
dispatch(fetchSource(startIndex, stopIndex))
let offset = 0;
for (let i = 0; i <= injectPoints.length; i++) {
if (injectPoints[i] <= startIndex) {
offset++;
}
}
dispatch(fetchSource(Math.min(startIndex - offset, 0), stopIndex))
lastRequest.current = { startIndex, stopIndex };
}, [dispatch]);
}, [dispatch, injectPoints]);

useEffect(() => {
if (scrollToRow !== null && !hasChecked && !isFetching) {
Expand Down
2 changes: 1 addition & 1 deletion src/js/component/item/items/table-row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ const TableRow = props => {

const itemData = useSelector(
state => itemKey ?
state.libraries[state.current.libraryKey].items[itemKey][Symbol.for('derived')]
state.libraries[state.current.libraryKey].dataObjects?.[itemKey]?.[Symbol.for('derived')]
: null
);

Expand Down
12 changes: 9 additions & 3 deletions src/js/component/item/items/table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const Table = () => {
const [isReordering, setIsReordering] = useState(false);
const [reorderTarget, setReorderTarget] = useState(null);
const [isHoveringBetweenRows, setIsHoveringBetweenRows] = useState(false);
const { isFetching, keys, hasChecked, totalResults, sortBy, sortDirection, requests } = useSourceData();
const { injectPoints, isFetching, keys, hasChecked, totalResults, sortBy, sortDirection, requests } = useSourceData();
const prevSortBy = usePrevious(sortBy);
const prevSortDirection = usePrevious(sortDirection);
const isAdvancedSearch = useSelector(state => state.current.isAdvancedSearch);
Expand Down Expand Up @@ -174,9 +174,15 @@ const Table = () => {
}, [keys, requests]);

const handleLoadMore = useCallback((startIndex, stopIndex) => {
dispatch(fetchSource(startIndex, stopIndex))
let offset = 0;
for(let i = 0; i <= injectPoints.length; i++) {
if(injectPoints[i] <= startIndex) {
offset++;
}
}
dispatch(fetchSource(Math.min(startIndex - offset, 0), stopIndex))
lastRequest.current = { startIndex, stopIndex };
}, [dispatch]);
}, [dispatch, injectPoints]);

const handleResize = useCallback(ev => {
const columnDom = ev.target.closest(['[data-colindex]']);
Expand Down
32 changes: 15 additions & 17 deletions src/js/component/libraries/collection-tree.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { COLLECTION_RENAME, COLLECTION_ADD, MOVE_COLLECTION } from '../../consta
import { createAttachmentsFromDropped, deleteCollection, toggleModal, updateCollection, navigate, triggerFocus } from '../../actions';
import { stopPropagation, getUniqueId } from '../../utils.js';

const makeDerivedData = (collections, path = [], opened, isTouchOrSmall) => {
const makeDerivedData = (collections = {}, path = [], opened, isTouchOrSmall) => {
const selectedParentKey = path[path.length - 2];
const childLookup = {};
const derivedData = Object.values(collections).reduce((aggr, c) => {
Expand Down Expand Up @@ -859,9 +859,7 @@ const CollectionTree = props => {
pickerState, ...rest } = props;
const dispatch = useDispatch();
const levelWrapperRef = useRef(null);
const collections = useSelector(
state => parentLibraryKey in state.libraries ? state.libraries[parentLibraryKey].collections.data : {}
);
const dataObjects = useSelector(state => state.libraries[parentLibraryKey]?.dataObjects);
const libraries = useSelector(state => state.config.libraries);
const isFetchingAllCollections = useSelector(
state => parentLibraryKey in state.libraries ? state.libraries[parentLibraryKey].collections.isFetchingAll : false
Expand All @@ -874,7 +872,7 @@ const CollectionTree = props => {
const prevSelectedCollectionKey = usePrevious(selectedCollectionKey);
const stateSelectedLibraryKey = useSelector(state => state.current.libraryKey);
const selectedLibraryKey = isPickerMode ? pickerState.libraryKey : stateSelectedLibraryKey;
const selectedCollection = collections[selectedCollectionKey];
const selectedCollection = dataObjects?.[selectedCollectionKey];
const prevSelectedCollection = usePrevious(selectedCollection);

const isTouchOrSmall = useSelector(state => state.device.isTouchOrSmall);
Expand All @@ -886,11 +884,11 @@ const CollectionTree = props => {

const isCurrentLibrary = parentLibraryKey === selectedLibraryKey;
const usesItemsNode = isSingleColumn && !isPickerMode;
const allCollections = useMemo(() => Object.values(collections), [collections]);
const allCollections = useMemo(() => Object.values(dataObjects ?? {}).filter(dataObject => dataObject[Symbol.for('type')] === 'collection'), [dataObjects]);

const path = useMemo(
() => makeCollectionsPath(selectedCollectionKey, collections, isCurrentLibrary),
[collections, isCurrentLibrary, selectedCollectionKey]
() => makeCollectionsPath(selectedCollectionKey, dataObjects ?? {}, isCurrentLibrary),
[dataObjects, isCurrentLibrary, selectedCollectionKey]
);

const [opened, setOpened] = useState(path.slice(0, -1));
Expand All @@ -899,13 +897,13 @@ const CollectionTree = props => {

const getParents = useCallback((collectionKey) => {
const parents = [];
let parentKey = collections[collectionKey]?.parentCollection;
let parentKey = dataObjects?.[collectionKey]?.parentCollection;
while(parentKey) {
parents.push(parentKey);
parentKey = collections[parentKey]?.parentCollection;
parentKey = dataObjects?.[parentKey]?.parentCollection;
}
return parents;
}, [collections]);
}, [dataObjects]);

const toggleOpen = useCallback((collectionKey, shouldOpen = null) => {
if(shouldOpen === null) {
Expand Down Expand Up @@ -934,17 +932,17 @@ const CollectionTree = props => {

const derivedData = useMemo(
() => {
return makeDerivedData(collections, path, opened, isTouchOrSmall);
return makeDerivedData(dataObjects, path, opened, isTouchOrSmall);
},
[collections, isTouchOrSmall, opened, path]
[dataObjects, isTouchOrSmall, opened, path]
);

const selectedDepth = path.length;
const selectedHasChildren = isCurrentLibrary && selectedCollectionKey && (derivedData[selectedCollectionKey] || {}).hasChildren;
const hasOpen = (selectedDepth > 0 && (selectedHasChildren || usesItemsNode)) || selectedDepth > 1;

const topLevelCollections = allCollections.filter(c => c.parentCollection === false );
const isLastLevel = usesItemsNode ? false : collections.length === 0;
const isLastLevel = usesItemsNode ? false : Object.keys(dataObjects ?? []).length === 0;

const shouldBeTabbableOnTouch = isCurrentLibrary && !selectedCollectionKey;
const shouldBeTabbable = shouldBeTabbableOnTouch || !isTouchOrSmall;
Expand All @@ -956,20 +954,20 @@ const CollectionTree = props => {
const parentsOfHighlighted = highlightedCollections.map(cKey => {
let parentCKeys = [];
do {
cKey = cKey in collections ? collections[cKey].parentCollection : null;
cKey = cKey in dataObjects ? dataObjects[cKey].parentCollection : null;
if (cKey) {
parentCKeys.push(cKey);
}
} while (cKey);
// if the highlighted collection or any of its parents is deleted, don't open any of them
return parentCKeys.some(cKey => collections[cKey]?.deleted) ? [] : parentCKeys;
return parentCKeys.some(cKey => dataObjects[cKey]?.deleted) ? [] : parentCKeys;
}).flat();

if(parentsOfHighlighted.length > 0) {
setOpened([...opened, ...parentsOfHighlighted]);
}
}
}, [collections, highlightedCollections, opened, prevHighlightedCollections]);
}, [dataObjects, highlightedCollections, opened, prevHighlightedCollections]);

useEffect(() => {
if(selectedCollectionKey !== prevSelectedCollectionKey) {
Expand Down
3 changes: 1 addition & 2 deletions src/js/component/modal/move-collections.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import deepEqual from 'deep-equal';
import Libraries from '../libraries';
import Modal from '../ui/modal';
import TouchHeader from '../touch-header.jsx';
import { get } from '../../utils';
import { MOVE_COLLECTION } from '../../constants/modals';
import { toggleModal, updateCollection } from '../../actions';
import { useNavigationState } from '../../hooks';
Expand All @@ -16,7 +15,7 @@ const MoveCollectionsModal = () => {
const collectionKey = useSelector(state => state.modal.collectionKey);
const libraryKey = useSelector(state => state.modal.libraryKey);
const currentParentCollectionKey = useSelector(
state => get(state, ['libraries', libraryKey, 'collections', 'data', collectionKey, 'parentCollection'], false)
state => state.libraries[libraryKey]?.dataObjects[collectionKey]?.parentCollection ?? false
);
const isOpen = useSelector(state => state.modal.id === MOVE_COLLECTION);
const isSingleColumn = useSelector(state => state.device.isSingleColumn);
Expand Down
4 changes: 2 additions & 2 deletions src/js/component/modal/new-collection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { Button, Icon } from 'web-common/components';
import Input from '../form/input';
import Modal from '../ui/modal';
import { COLLECTION_ADD } from '../../constants/modals';
import { get, getUniqueId } from '../../utils';
import { getUniqueId } from '../../utils';
import { toggleModal, createCollection } from '../../actions';

const NewCollectionModal = () => {
const dispatch = useDispatch();
const [name, setName] = useState('');
const libraryKey = useSelector(state => state.current.libraryKey);
const parentCollection = useSelector(state =>
get(state, ['libraries', libraryKey, 'collections', 'data', state.modal.parentCollectionKey]),
state.libraries[libraryKey]?.dataObjects[state.modal.parentCollectionKey],
shallowEqual
);

Expand Down
Loading

0 comments on commit e8138ba

Please sign in to comment.