1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
import axios from '~/lib/utils/axios_utils';
/**
* This takes an array of project or groups that were stored in the local storage, to be shown in
* the context switcher, and sorts them by frequency and last access date.
* In the resulting array, the most popular item (highest frequency and most recent access date) is
* placed at the first index, while the least popular is at the last index.
*
* @param {Array} items The projects or groups stored in the local storage
* @returns The items, sorted by frequency and last access date
*/
const sortItemsByFrequencyAndLastAccess = (items) =>
items.sort((itemA, itemB) => {
// Sort all frequent items in decending order of frequency
// and then by lastAccessedOn with recent most first
if (itemA.frequency !== itemB.frequency) {
return itemB.frequency - itemA.frequency;
}
if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
return itemB.lastAccessedOn - itemA.lastAccessedOn;
}
return 0;
});
// This imitates getTopFrequentItems from app/assets/javascripts/frequent_items/utils.js, but
// adjusts the rules to accommodate for the context switcher's designs.
export const getTopFrequentItems = (items, maxCount) => {
if (!Array.isArray(items)) return [];
const frequentItems = items.filter((item) => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY);
sortItemsByFrequencyAndLastAccess(frequentItems);
return frequentItems.slice(0, maxCount);
};
/**
* This tracks projects' and groups' visits in order to suggest a list of frequently visited
* entities to the user. Currently, this track visits in two ways:
* - The legacy approach uses a simple counting algorithm and stores the data in the local storage.
* - The above approach is being migrated to a backend-based one, where visits will be stored in the
* DB, and suggestions will be made through a smarter algorithm. When we are ready to transition
* to the newer approach, the legacy one will be cleaned up.
* @param {object} item The project/group item being tracked.
* @param {string} namespace A string indicating whether the tracked entity is a project or a group.
* @param {string} trackVisitsPath The API endpoint to track visits server-side.
* @returns {void}
*/
const updateItemAccess = (
contextItem,
{ lastAccessedOn, frequency = 0 } = {},
namespace,
trackVisitsPath,
) => {
const now = Date.now();
const neverAccessed = !lastAccessedOn;
const shouldUpdate = neverAccessed || Math.abs(now - lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1;
if (shouldUpdate) {
axios({
url: trackVisitsPath,
method: 'POST',
data: {
type: namespace,
id: contextItem.id,
},
}).catch((e) => {
Sentry.captureException(e);
});
}
return {
...contextItem,
frequency: shouldUpdate ? frequency + 1 : frequency,
lastAccessedOn: shouldUpdate ? now : lastAccessedOn,
};
};
export const trackContextAccess = (username, context, trackVisitsPath) => {
if (!AccessorUtilities.canUseLocalStorage()) {
return false;
}
const storageKey = `${username}/frequent-${context.namespace}`;
const storedRawItems = localStorage.getItem(storageKey);
const storedItems = storedRawItems ? JSON.parse(storedRawItems) : [];
const existingItemIndex = storedItems.findIndex(
(cachedItem) => cachedItem.id === context.item.id,
);
if (existingItemIndex > -1) {
storedItems[existingItemIndex] = updateItemAccess(
context.item,
storedItems[existingItemIndex],
context.namespace,
trackVisitsPath,
);
} else {
const newItem = updateItemAccess(
context.item,
storedItems[existingItemIndex],
context.namespace,
trackVisitsPath,
);
if (storedItems.length === FREQUENT_ITEMS.MAX_COUNT) {
sortItemsByFrequencyAndLastAccess(storedItems);
storedItems.pop();
}
storedItems.push(newItem);
}
return localStorage.setItem(storageKey, JSON.stringify(storedItems));
};
export const getItemsFromLocalStorage = ({ storageKey, maxItems }) => {
if (!AccessorUtilities.canUseLocalStorage()) {
return [];
}
try {
const parsedCachedFrequentItems = JSON.parse(localStorage.getItem(storageKey));
return getTopFrequentItems(parsedCachedFrequentItems, maxItems);
} catch (e) {
Sentry.captureException(e);
return [];
}
};
export const removeItemFromLocalStorage = ({ storageKey, item }) => {
try {
const parsedCachedFrequentItems = JSON.parse(localStorage.getItem(storageKey));
const filteredItems = parsedCachedFrequentItems.filter((i) => i.id !== item.id);
localStorage.setItem(storageKey, JSON.stringify(filteredItems));
return filteredItems;
} catch (e) {
Sentry.captureException(e);
return [];
}
};
export const ariaCurrent = (isActive) => (isActive ? 'page' : null);
|