Skip to content

Commit

Permalink
Improve tag swatches in the items table. #555 #541
Browse files Browse the repository at this point in the history
* Display emojis for colorless tags in the items tree.
* Sort colored tags based on their position.
* Extend tests to cover this functionality.
* Improve appearance of the colored circles and crescents.
  • Loading branch information
tnajdek committed Aug 8, 2024
1 parent 47deefa commit ebda887
Show file tree
Hide file tree
Showing 29 changed files with 32,329 additions and 122 deletions.
36 changes: 23 additions & 13 deletions src/js/common/item.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cleanDOI, cleanURL, get, isOnlyEmoji } from '../utils';
import { cleanDOI, cleanURL, get, containsEmoji, extractEmoji } from '../utils';
import { noteAsTitle, itemTypeLocalized, dateLocalized } from './format';
import { itemTypesWithIcons } from '../../../data/item-types-with-icons.json';

Expand Down Expand Up @@ -98,19 +98,29 @@ const getDerivedData = (mappings, item, itemTypes, tagColors) => {
const date = item[Symbol.for('meta')] && item[Symbol.for('meta')].parsedDate ?
item[Symbol.for('meta')].parsedDate :
'';

const colors = [];
const emojis = [];
const colors = item.tags.reduce(
(acc, { tag }) => {
if(tag in tagColors) {
if(isOnlyEmoji(tag)) {
emojis.push(tag);
} else {
acc.push(tagColors[tag]);
}
}
return acc;
}, []
);

// 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)) {
return;
}
if (containsEmoji(name)) {
emojis.push(extractEmoji(name));
} else {
colors.push(color);
}
});

// non-colored tags containing emoji, sorted alphabetically (item.tags should already be sorted)
item.tags.forEach(({ tag }) => {
if (!(tag in tagColors.lookup) && containsEmoji(tag)) {
emojis.push(extractEmoji(tag));
}
});

const createdByUser = item[Symbol.for('meta')] && item[Symbol.for('meta')].createdByUser ?
item[Symbol.for('meta')].createdByUser.username :
'';
Expand Down
6 changes: 4 additions & 2 deletions src/js/component/item/items/list-row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ const ListRow = memo(props => {
height="12"
/></Fragment>) // eslint-disable-line no-irregular-whitespace
}
{ emojis.join(' ') }
{ emojis.map(emoji => {
return <span key={emoji} className="emoji">{emoji}</span>
}) }
<span className="tag-circles">
{ colors.map((color, index) => (
<Icon
Expand All @@ -183,7 +185,7 @@ const ListRow = memo(props => {
(!isSelectMode && isActive) ? 'circle-active' : 'circle' :
(!isSelectMode && isActive) ? 'crescent-circle-active' : 'crescent-circle'
}
width={ index === 0 ? 12 : 8 }
width="12"
height="12"
data-color={ color.toLowerCase() }
style={ { color } }
Expand Down
14 changes: 8 additions & 6 deletions src/js/component/item/items/table-row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,21 @@ const TitleCell = memo(props => {
/>
<div className="truncate" id={labelledById} dangerouslySetInnerHTML={ { __html: formattedSpan.outerHTML } } />
<div className="tag-colors">
{ itemData.emojis.join(' ') }
{ itemData.emojis.map(emoji => {
return <span key={ emoji } className="emoji">{ emoji }</span>
}) }
<span className="tag-circles">
{ itemData.colors.map((color, index) => (
<Icon
label={ `${colorNames[color] || ''} circle icon` }
key={ index }
type={ index === 0 ? '10/circle' : '10/crescent-circle' }
type={ index === 0 ? '12/circle' : '12/crescent-circle' }
symbol={ index === 0 ?
(isFocused && isSelected ? 'circle-focus' : 'circle') :
(isFocused && isSelected ? 'crescent-circle-focus' : 'crescent-circle')
(isFocused && isSelected ? 'circle-active' : 'circle') :
(isFocused && isSelected ? 'crescent-circle-active' : 'crescent-circle')
}
width={ index === 0 ? 10 : 7 }
height="10"
width="12"
height="12"
data-color={ color.toLowerCase() }
style={ { color } }
/>
Expand Down
4 changes: 2 additions & 2 deletions src/js/component/tag-selector/tag-selector-items.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useFocusManager, usePrevious } from 'web-common/hooks';
import { isTriggerEvent } from 'web-common/utils';

import { connectionIssues, checkColoredTags, fetchTags, navigate } from '../../actions';
import { get, isOnlyEmoji } from '../../utils';
import { containsEmoji, get } from '../../utils';
import { ITEM } from '../../constants/dnd';
import { useSourceSignature, useTags } from '../../hooks';

Expand All @@ -31,7 +31,7 @@ const Tag = memo(props => {
disabled: tag.disabled,
selected: tag.selected,
colored: tag.color,
emoji: isOnlyEmoji(tag.tag),
emoji: containsEmoji(tag.tag),
placeholder: tag.isPlaceholder,
'dnd-target': isOver && canDrop
}) }
Expand Down
2 changes: 1 addition & 1 deletion src/js/reducers/libraries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const libraries = (state = {}, action, { itemsPublications, meta } = {}) => {
creating: creating(state[action.libraryKey]?.creating, action),
deleting: deleting(get(state, [action.libraryKey, 'deleting']), action),
items: items(get(state, [action.libraryKey, 'items']), action, {
tagColors: state[action.libraryKey]?.tagColors?.lookup, meta
tagColors: (state[action.libraryKey]?.tagColors || {}), meta
} ),
itemsByCollection: itemsByCollection(get(state, [action.libraryKey, 'itemsByCollection']), action, {
items: (state[action.libraryKey] || {}).items, meta
Expand Down
16 changes: 8 additions & 8 deletions src/js/reducers/libraries/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ const discardIfOldVersion = (newItems, oldItems) => {
return result;
}

const items = (state = {}, action, metaAndTags) => {
const items = (state = {}, action, { meta, tagColors }) => {
switch(action.type) {
case RECEIVE_CREATE_ITEM:
return {
...state,
[action.item.key]: calculateDerivedData(action.item, metaAndTags)
[action.item.key]: calculateDerivedData(action.item, { meta, tagColors })
};
case RECEIVE_DELETE_ITEM:
return omit(state, action.item.key);
Expand All @@ -48,7 +48,7 @@ const items = (state = {}, action, metaAndTags) => {
[action.itemKey]: calculateDerivedData({
...get(state, action.itemKey, {}),
...action.item
}, metaAndTags)
}, { meta, tagColors })
};
case RECEIVE_UPDATE_MULTIPLE_ITEMS:
case RECEIVE_MOVE_ITEMS_TRASH:
Expand All @@ -61,7 +61,7 @@ const items = (state = {}, action, metaAndTags) => {
...indexByKey(Object.values(action.items), 'key', item => (calculateDerivedData({
...state[item.key],
...item
}, metaAndTags)))
}, { meta, tagColors })))
}
case RECEIVE_CHILD_ITEMS:
case RECEIVE_CREATE_ITEMS:
Expand All @@ -75,7 +75,7 @@ const items = (state = {}, action, metaAndTags) => {
case RECEIVE_TRASH_ITEMS:
return {
...state,
...discardIfOldVersion(indexByKey(calculateDerivedData(action.items, metaAndTags), 'key'), state)
...discardIfOldVersion(indexByKey(calculateDerivedData(action.items, { meta, tagColors }), 'key'), state)
};
case RECEIVE_UPLOAD_ATTACHMENT:
return {
Expand Down Expand Up @@ -104,12 +104,12 @@ const items = (state = {}, action, metaAndTags) => {
case RECEIVE_LIBRARY_SETTINGS:
case RECEIVE_UPDATE_LIBRARY_SETTINGS:
return action.settingsKey === 'tagColors' ? mapObject(state, (itemKey, item) => [itemKey, calculateDerivedData(item, {
meta: metaAndTags.meta,
tagColors: indexByKey(action.value ?? [], 'name', ({ color }) => color)
meta,
tagColors: { value: action.value ?? [], lookup: indexByKey(action.value ?? [], 'name', ({ color }) => color)}
})]) : state;
case RECEIVE_DELETE_LIBRARY_SETTINGS:
return action.settingsKey === 'tagColors' ? mapObject(state, (itemKey, item) => [itemKey, calculateDerivedData(item, {
meta: metaAndTags.meta,
meta,
tagColors: {}
})]) : state;
case RECEIVE_DELETE_TAGS:
Expand Down
20 changes: 14 additions & 6 deletions src/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,13 +474,20 @@ const getPrevSibling = (elem, selector) => {
}
};

// https://github.com/zotero/zotero/blob/256bd157edd7707aa1affa1822f68f41be1f988c/chrome/content/zotero/xpcom/utilities_internal.js#L408
const isOnlyEmoji = str => {
// Remove emoji, Zero Width Joiner, and Variation Selector-16 and see if anything's left
const re = /\p{Extended_Pictographic}|\u200D|\uFE0F/gu;
return !str.replace(re, '');
// https://github.com/zotero/zotero/blob/214a668286d35a630db293bb835f544693698297/chrome/content/zotero/xpcom/utilities_internal.js#L399-L408
const containsEmoji = str => {
let re = /\p{Extended_Pictographic}/gu;
return !!str.match(re);
}

// https://github.com/abaevbog/zotero/blob/f8fd90945663d4745306d88be298633f8c7229de/chrome/content/zotero/xpcom/data/tags.js#L1000
const extractEmoji = str => {
// Split by anything that is not an emoji, Zero Width Joiner, or Variation Selector-16
// And return first continuous span of emojis
let re = /[^\p{Extended_Pictographic}\u200D\uFE0F]+/gu; //eslint-disable-line no-misleading-character-class
return str.split(re).filter(Boolean)[0] || null;
};

const isReaderCompatibleBrowser = () => typeof structuredClone === "function";

export {
Expand All @@ -491,10 +498,12 @@ export {
cleanURL,
compare,
compareItem,
containsEmoji,
deduplicate,
deduplicateByHash,
deduplicateByKey,
enumerateObjects,
extractEmoji,
get,
getAbortController,
getDOIURL,
Expand All @@ -512,7 +521,6 @@ export {
indexByGeneratedKey,
indexByKey,
isLikeURL,
isOnlyEmoji,
isReaderCompatibleBrowser,
JSONTryParse,
localStorageWrapper,
Expand Down
6 changes: 3 additions & 3 deletions src/scss/components/_icon-extra.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
}
}

.icon-crescent-circle,
.icon-crescent-circle-active {
margin-left: -2px;
.icon-crescent-circle {
margin-left: -5px;
}


.icon-circle, .icon-crescent-circle {
color: $icon-circle-color;

Expand Down
4 changes: 4 additions & 0 deletions src/scss/components/item/_list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@
display: none;
}

.emoji {
margin-left: 4px;
}

.tag-circles {
display: inline-flex;
margin-left: $space-min;
Expand Down
8 changes: 7 additions & 1 deletion src/scss/components/item/_table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,14 @@
overflow: hidden;
padding-left: $space-xs;

.emoji {
margin-left: 4px;
}

.tag-circles {
display: inline-flex;
// match the behaviour of the client where tag swatches are displayed flipped
// https://github.com/zotero/zotero/blob/214a668286d35a630db293bb835f544693698297/scss/components/_item-tree.scss#L106
transform: scaleX(-1);
margin-left: $space-min;
}
}
Expand Down
13 changes: 0 additions & 13 deletions src/static/icons/10/circle.svg

This file was deleted.

13 changes: 0 additions & 13 deletions src/static/icons/10/crescent-circle.svg

This file was deleted.

2 changes: 0 additions & 2 deletions src/static/icons/12/circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 6 additions & 8 deletions src/static/icons/12/crescent-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions test/fixtures/response/test-user-get-items-golden-tags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[
{
"tag": "cute",
"links": {
"self": {
"href": "https://api.zotero.org/users/1/tags/cute",
"type": "application/json"
},
"alternate": {
"href": "http://zotero.org/users/1/tags/cute",
"type": "text/html"
}
},
"meta": {
"type": 0,
"numItems": 7
}
},
{
"tag": "dogs 🐕",
"links": {
"self": {
"href": "https://api.zotero.org/users/1/tags/dogs+%F0%9F%90%95",
"type": "application/json"
},
"alternate": {
"href": "http://zotero.org/users/1/tags/dogs+%F0%9F%90%95",
"type": "text/html"
}
},
"meta": {
"type": 0,
"numItems": 3
}
},
{
"tag": "⭐️",
"links": {
"self": {
"href": "https://api.zotero.org/users/1/tags/%E2%AD%90%EF%B8%8F",
"type": "application/json"
},
"alternate": {
"href": "http://zotero.org/users/1/tags/%E2%AD%90%EF%B8%8F",
"type": "text/html"
}
},
"meta": {
"type": 0,
"numItems": 5
}
}
]
Loading

0 comments on commit ebda887

Please sign in to comment.