feeds-fun 1.14.2 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/DiscoveryForm.vue +36 -17
- package/src/components/EntriesList.vue +5 -10
- package/src/components/EntryForList.vue +20 -39
- package/src/components/FfunTag.vue +55 -30
- package/src/components/OPMLUpload.vue +7 -0
- package/src/components/RuleConstructor.vue +56 -27
- package/src/components/RuleForList.vue +14 -3
- package/src/components/TagsFilter.vue +68 -76
- package/src/components/TagsList.vue +14 -66
- package/src/components/notifications/Block.vue +7 -2
- package/src/components/notifications/CreateRuleHelp.vue +16 -9
- package/src/inputs/ScoreSelector.vue +10 -3
- package/src/layouts/SidePanelLayout.vue +2 -2
- package/src/logic/api.ts +33 -5
- package/src/logic/asserts.ts +5 -0
- package/src/logic/tagsFilterState.ts +63 -11
- package/src/logic/types.ts +34 -7
- package/src/stores/entries.ts +27 -1
- package/src/stores/globalSettings.ts +0 -2
- package/src/values/Score.vue +11 -2
- package/src/views/DiscoveryView.vue +16 -6
- package/src/views/FeedsView.vue +9 -0
- package/src/views/NewsView.vue +51 -51
- package/src/views/RulesView.vue +34 -14
package/src/logic/types.ts
CHANGED
|
@@ -231,7 +231,9 @@ export function entryFromJSON(
|
|
|
231
231
|
|
|
232
232
|
export type Rule = {
|
|
233
233
|
readonly id: RuleId;
|
|
234
|
-
readonly
|
|
234
|
+
readonly requiredTags: string[];
|
|
235
|
+
readonly excludedTags: string[];
|
|
236
|
+
readonly allTags: string[];
|
|
235
237
|
readonly score: number;
|
|
236
238
|
readonly createdAt: Date;
|
|
237
239
|
readonly updatedAt: Date;
|
|
@@ -239,22 +241,27 @@ export type Rule = {
|
|
|
239
241
|
|
|
240
242
|
export function ruleFromJSON({
|
|
241
243
|
id,
|
|
242
|
-
|
|
244
|
+
requiredTags,
|
|
245
|
+
excludedTags,
|
|
243
246
|
score,
|
|
244
247
|
createdAt,
|
|
245
248
|
updatedAt
|
|
246
249
|
}: {
|
|
247
250
|
id: string;
|
|
248
|
-
|
|
251
|
+
requiredTags: string[];
|
|
252
|
+
excludedTags: string[];
|
|
249
253
|
score: number;
|
|
250
254
|
createdAt: string;
|
|
251
255
|
updatedAt: string;
|
|
252
256
|
}): Rule {
|
|
253
|
-
|
|
257
|
+
requiredTags = requiredTags.sort();
|
|
258
|
+
excludedTags = excludedTags.sort();
|
|
254
259
|
|
|
255
260
|
return {
|
|
256
261
|
id: toRuleId(id),
|
|
257
|
-
|
|
262
|
+
requiredTags: requiredTags,
|
|
263
|
+
excludedTags: excludedTags,
|
|
264
|
+
allTags: requiredTags.concat(excludedTags),
|
|
258
265
|
score: score,
|
|
259
266
|
createdAt: new Date(createdAt),
|
|
260
267
|
updatedAt: new Date(updatedAt)
|
|
@@ -287,24 +294,28 @@ export type FeedInfo = {
|
|
|
287
294
|
readonly title: string;
|
|
288
295
|
readonly description: string;
|
|
289
296
|
readonly entries: EntryInfo[];
|
|
297
|
+
readonly isLinked: boolean;
|
|
290
298
|
};
|
|
291
299
|
|
|
292
300
|
export function feedInfoFromJSON({
|
|
293
301
|
url,
|
|
294
302
|
title,
|
|
295
303
|
description,
|
|
296
|
-
entries
|
|
304
|
+
entries,
|
|
305
|
+
isLinked
|
|
297
306
|
}: {
|
|
298
307
|
url: string;
|
|
299
308
|
title: string;
|
|
300
309
|
description: string;
|
|
301
310
|
entries: any[];
|
|
311
|
+
isLinked: boolean;
|
|
302
312
|
}): FeedInfo {
|
|
303
313
|
return {
|
|
304
314
|
url: toURL(url),
|
|
305
315
|
title,
|
|
306
316
|
description,
|
|
307
|
-
entries: entries.map(entryInfoFromJSON)
|
|
317
|
+
entries: entries.map(entryInfoFromJSON),
|
|
318
|
+
isLinked
|
|
308
319
|
};
|
|
309
320
|
}
|
|
310
321
|
|
|
@@ -478,3 +489,19 @@ export function collectionFeedInfoFromJSON({
|
|
|
478
489
|
id: toFeedId(id)
|
|
479
490
|
});
|
|
480
491
|
}
|
|
492
|
+
|
|
493
|
+
export class ApiMessage {
|
|
494
|
+
readonly type: string;
|
|
495
|
+
readonly code: string;
|
|
496
|
+
readonly message: string;
|
|
497
|
+
|
|
498
|
+
constructor({type, code, message}: {type: string; code: string; message: string}) {
|
|
499
|
+
this.type = type;
|
|
500
|
+
this.code = code;
|
|
501
|
+
this.message = message;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function apiMessageFromJSON({type, code, message}: {type: string; code: string; message: string}): ApiMessage {
|
|
506
|
+
return new ApiMessage({type, code, message});
|
|
507
|
+
}
|
package/src/stores/entries.ts
CHANGED
|
@@ -8,12 +8,14 @@ import * as api from "@/logic/api";
|
|
|
8
8
|
import {Timer} from "@/logic/timer";
|
|
9
9
|
import {computedAsync} from "@vueuse/core";
|
|
10
10
|
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
11
|
+
import * as events from "@/logic/events";
|
|
11
12
|
|
|
12
13
|
export const useEntriesStore = defineStore("entriesStore", () => {
|
|
13
14
|
const globalSettings = useGlobalSettingsStore();
|
|
14
15
|
|
|
15
16
|
const entries = ref<{[key: t.EntryId]: t.Entry}>({});
|
|
16
17
|
const requestedEntries = ref<{[key: t.EntryId]: boolean}>({});
|
|
18
|
+
const displayedEntryId = ref<t.EntryId | null>(null);
|
|
17
19
|
|
|
18
20
|
function registerEntry(entry: t.Entry) {
|
|
19
21
|
if (entry.id in entries.value) {
|
|
@@ -95,11 +97,35 @@ export const useEntriesStore = defineStore("entriesStore", () => {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
async function displayEntry({entryId}: {entryId: t.EntryId}) {
|
|
101
|
+
displayedEntryId.value = entryId;
|
|
102
|
+
|
|
103
|
+
requestFullEntry({entryId: entryId});
|
|
104
|
+
|
|
105
|
+
if (!entries.value[entryId].hasMarker(e.Marker.Read)) {
|
|
106
|
+
await setMarker({
|
|
107
|
+
entryId: entryId,
|
|
108
|
+
marker: e.Marker.Read
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await events.newsBodyOpened({entryId: entryId});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hideEntry({entryId}: {entryId: t.EntryId}) {
|
|
116
|
+
if (displayedEntryId.value === entryId) {
|
|
117
|
+
displayedEntryId.value = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
return {
|
|
99
122
|
entries,
|
|
100
123
|
requestFullEntry,
|
|
101
124
|
setMarker,
|
|
102
125
|
removeMarker,
|
|
103
|
-
loadedEntriesReport
|
|
126
|
+
loadedEntriesReport,
|
|
127
|
+
displayedEntryId,
|
|
128
|
+
displayEntry,
|
|
129
|
+
hideEntry
|
|
104
130
|
};
|
|
105
131
|
});
|
|
@@ -18,7 +18,6 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
|
|
|
18
18
|
// Entries
|
|
19
19
|
const lastEntriesPeriod = ref(e.LastEntriesPeriod.Day3);
|
|
20
20
|
const entriesOrder = ref(e.EntriesOrder.Score);
|
|
21
|
-
const showEntriesTags = ref(true);
|
|
22
21
|
const showRead = ref(true);
|
|
23
22
|
|
|
24
23
|
// Feeds
|
|
@@ -64,7 +63,6 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
|
|
|
64
63
|
mainPanelMode,
|
|
65
64
|
lastEntriesPeriod,
|
|
66
65
|
entriesOrder,
|
|
67
|
-
showEntriesTags,
|
|
68
66
|
showRead,
|
|
69
67
|
dataVersion,
|
|
70
68
|
updateDataVersion,
|
package/src/values/Score.vue
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
for (const rule of rules) {
|
|
31
31
|
const tags = [];
|
|
32
32
|
|
|
33
|
-
for (const tagId of rule.
|
|
33
|
+
for (const tagId of rule.requiredTags) {
|
|
34
34
|
const tagInfo = tagsStore.tags[tagId];
|
|
35
35
|
if (tagInfo) {
|
|
36
36
|
tags.push(tagInfo.name);
|
|
@@ -39,7 +39,16 @@
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
for (const tagId of rule.excludedTags) {
|
|
43
|
+
const tagInfo = tagsStore.tags[tagId];
|
|
44
|
+
if (tagInfo) {
|
|
45
|
+
tags.push("NOT " + tagInfo.name);
|
|
46
|
+
} else {
|
|
47
|
+
tags.push("NOT " + tagId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
strings.push(rule.score.toString().padStart(2, " ") + " — " + tags.join(" AND "));
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
alert(strings.join("\n"));
|
|
@@ -10,16 +10,26 @@
|
|
|
10
10
|
|
|
11
11
|
<h2>Lood feeds from an OPML file</h2>
|
|
12
12
|
|
|
13
|
+
<div class="ffun-info-good">
|
|
14
|
+
<p>
|
|
15
|
+
<a
|
|
16
|
+
href="https://en.wikipedia.org/wiki/OPML"
|
|
17
|
+
target="_blank"
|
|
18
|
+
>OPML</a
|
|
19
|
+
>
|
|
20
|
+
is a widely-used format for transferring news feed lists between platforms.
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<p
|
|
24
|
+
>Export your feeds from your old reader in OPML format and import them into our reader to seamlessly
|
|
25
|
+
transition!</p
|
|
26
|
+
>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
13
29
|
<opml-upload />
|
|
14
30
|
|
|
15
31
|
<h2>Search for a feed</h2>
|
|
16
32
|
|
|
17
|
-
<div class="ffun-info-attention">
|
|
18
|
-
<p> The discovery feature is experimental and might not work on all websites. </p>
|
|
19
|
-
|
|
20
|
-
<p> If we can’t find a feed for a site, try finding the feed's URL manually, then enter it in the form. </p>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
33
|
<discovery-form />
|
|
24
34
|
</side-panel-layout>
|
|
25
35
|
</template>
|
package/src/views/FeedsView.vue
CHANGED
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
off-text="last" />
|
|
26
26
|
</template>
|
|
27
27
|
|
|
28
|
+
<template #side-menu-item-4>
|
|
29
|
+
<a
|
|
30
|
+
class="ffun-form-button p-1 my-1 block w-full text-center"
|
|
31
|
+
href="/api/get-opml"
|
|
32
|
+
target="_blank"
|
|
33
|
+
>Download OPML</a
|
|
34
|
+
>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
28
37
|
<template #main-header>
|
|
29
38
|
Feeds
|
|
30
39
|
<span v-if="sortedFeeds"> [{{ sortedFeeds.length }}] </span>
|
package/src/views/NewsView.vue
CHANGED
|
@@ -15,15 +15,6 @@
|
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
17
|
<template #side-menu-item-3>
|
|
18
|
-
Show tags:
|
|
19
|
-
<config-flag
|
|
20
|
-
style="min-width: 2.5rem"
|
|
21
|
-
v-model:flag="globalSettings.showEntriesTags"
|
|
22
|
-
on-text="no"
|
|
23
|
-
off-text="yes" />
|
|
24
|
-
</template>
|
|
25
|
-
|
|
26
|
-
<template #side-menu-item-4>
|
|
27
18
|
Show read:
|
|
28
19
|
<config-flag
|
|
29
20
|
style="min-width: 2.5rem"
|
|
@@ -35,7 +26,7 @@
|
|
|
35
26
|
<template #side-footer>
|
|
36
27
|
<tags-filter
|
|
37
28
|
:tags="tagsCount"
|
|
38
|
-
|
|
29
|
+
:show-create-rule="true" />
|
|
39
30
|
</template>
|
|
40
31
|
|
|
41
32
|
<template #main-header>
|
|
@@ -55,16 +46,14 @@
|
|
|
55
46
|
<entries-list
|
|
56
47
|
:entriesIds="entriesReport"
|
|
57
48
|
:time-field="timeField"
|
|
58
|
-
:show-tags="globalSettings.showEntriesTags"
|
|
59
49
|
:tags-count="tagsCount"
|
|
60
50
|
:showFromStart="25"
|
|
61
|
-
:showPerPage="25"
|
|
62
|
-
@entry:bodyVisibilityChanged="onBodyVisibilityChanged" />
|
|
51
|
+
:showPerPage="25" />
|
|
63
52
|
</side-panel-layout>
|
|
64
53
|
</template>
|
|
65
54
|
|
|
66
55
|
<script lang="ts" setup>
|
|
67
|
-
import {computed, ref, onUnmounted, watch} from "vue";
|
|
56
|
+
import {computed, ref, onUnmounted, watch, provide} from "vue";
|
|
68
57
|
import {computedAsync} from "@vueuse/core";
|
|
69
58
|
import * as api from "@/logic/api";
|
|
70
59
|
import * as tagsFilterState from "@/logic/tagsFilterState";
|
|
@@ -79,67 +68,86 @@
|
|
|
79
68
|
|
|
80
69
|
const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
|
|
81
70
|
|
|
71
|
+
provide("tagsStates", tagsStates);
|
|
72
|
+
|
|
82
73
|
globalSettings.mainPanelMode = e.MainPanelMode.Entries;
|
|
83
74
|
|
|
84
75
|
globalSettings.updateDataVersion();
|
|
85
76
|
|
|
86
77
|
const entriesWithOpenedBody = ref<{[key: t.EntryId]: boolean}>({});
|
|
87
78
|
|
|
88
|
-
const
|
|
79
|
+
const _sortedEntries = computed(() => {
|
|
89
80
|
if (entriesStore.loadedEntriesReport === null) {
|
|
90
81
|
return [];
|
|
91
82
|
}
|
|
92
83
|
|
|
93
|
-
|
|
84
|
+
const orderProperties = e.EntriesOrderProperties.get(globalSettings.entriesOrder);
|
|
94
85
|
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
if (entriesWithOpenedBody.value[entryId]) {
|
|
98
|
-
// always show read entries with open body
|
|
99
|
-
// otherwise, they will hide right after opening it
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
return !entriesStore.entries[entryId].hasMarker(e.Marker.Read);
|
|
103
|
-
});
|
|
86
|
+
if (orderProperties === undefined) {
|
|
87
|
+
throw new Error(`Unknown order ${globalSettings.entriesOrder}`);
|
|
104
88
|
}
|
|
105
89
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
report = report.sort((a: t.EntryId, b: t.EntryId) => {
|
|
109
|
-
const orderProperties = e.EntriesOrderProperties.get(globalSettings.entriesOrder);
|
|
90
|
+
const field = orderProperties.orderField;
|
|
91
|
+
const direction = orderProperties.direction;
|
|
110
92
|
|
|
111
|
-
|
|
112
|
-
throw new Error(`Unknown order ${globalSettings.entriesOrder}`);
|
|
113
|
-
}
|
|
93
|
+
// let report = entriesStore.loadedEntriesReport.slice();
|
|
114
94
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
// Pre-map to avoid repeated lookups in the comparator
|
|
96
|
+
const mapped = entriesStore.loadedEntriesReport.map((entryId) => {
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
return {entryId, value: entriesStore.entries[entryId][field]};
|
|
99
|
+
});
|
|
119
100
|
|
|
120
|
-
|
|
101
|
+
mapped.sort((a: {entryId: t.EntryId; value: any}, b: {entryId: t.EntryId; value: any}) => {
|
|
102
|
+
if (a.value === null && b.value === null) {
|
|
121
103
|
return 0;
|
|
122
104
|
}
|
|
123
105
|
|
|
124
|
-
if (
|
|
106
|
+
if (a.value === null) {
|
|
125
107
|
return 1;
|
|
126
108
|
}
|
|
127
109
|
|
|
128
|
-
if (
|
|
110
|
+
if (b.value === null) {
|
|
129
111
|
return -1;
|
|
130
112
|
}
|
|
131
113
|
|
|
132
|
-
if (
|
|
133
|
-
return
|
|
114
|
+
if (a.value < b.value) {
|
|
115
|
+
return direction;
|
|
134
116
|
}
|
|
135
117
|
|
|
136
|
-
if (
|
|
137
|
-
return -
|
|
118
|
+
if (a.value > b.value) {
|
|
119
|
+
return -direction;
|
|
138
120
|
}
|
|
139
121
|
|
|
140
122
|
return 0;
|
|
141
123
|
});
|
|
142
124
|
|
|
125
|
+
const report = mapped.map((x) => x.entryId);
|
|
126
|
+
|
|
127
|
+
return report;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const _visibleEntries = computed(() => {
|
|
131
|
+
let report = _sortedEntries.value.slice();
|
|
132
|
+
|
|
133
|
+
if (!globalSettings.showRead) {
|
|
134
|
+
report = report.filter((entryId) => {
|
|
135
|
+
if (entriesStore.displayedEntryId == entryId) {
|
|
136
|
+
// always show read entries with open body
|
|
137
|
+
// otherwise, they will hide right after opening it
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return !entriesStore.entries[entryId].hasMarker(e.Marker.Read);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return report;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const entriesReport = computed(() => {
|
|
148
|
+
let report = _visibleEntries.value.slice();
|
|
149
|
+
|
|
150
|
+
report = tagsStates.value.filterByTags(report, (entryId) => entriesStore.entries[entryId].tags);
|
|
143
151
|
return report;
|
|
144
152
|
});
|
|
145
153
|
|
|
@@ -193,14 +201,6 @@
|
|
|
193
201
|
|
|
194
202
|
return orderProperties.timeField;
|
|
195
203
|
});
|
|
196
|
-
|
|
197
|
-
function onTagStateChanged({tag, state}: {tag: string; state: tagsFilterState.State}) {
|
|
198
|
-
tagsStates.value.onTagStateChanged({tag, state});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function onBodyVisibilityChanged({entryId, visible}: {entryId: t.EntryId; visible: boolean}) {
|
|
202
|
-
entriesWithOpenedBody.value[entryId] = visible;
|
|
203
|
-
}
|
|
204
204
|
</script>
|
|
205
205
|
|
|
206
206
|
<style></style>
|
package/src/views/RulesView.vue
CHANGED
|
@@ -13,11 +13,21 @@
|
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
15
|
<template #side-footer>
|
|
16
|
-
<tags-filter
|
|
17
|
-
:tags="tags"
|
|
18
|
-
@tag:stateChanged="onTagStateChanged" />
|
|
16
|
+
<tags-filter :tags="tagsCount" />
|
|
19
17
|
</template>
|
|
20
18
|
|
|
19
|
+
<div class="ffun-info-good">
|
|
20
|
+
<p
|
|
21
|
+
>You can create new rules on the
|
|
22
|
+
<a
|
|
23
|
+
href="#"
|
|
24
|
+
@click.prevent="goToNews()"
|
|
25
|
+
>news</a
|
|
26
|
+
>
|
|
27
|
+
tab.</p
|
|
28
|
+
>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
21
31
|
<rules-list
|
|
22
32
|
v-if="rules"
|
|
23
33
|
:rules="sortedRules" />
|
|
@@ -25,7 +35,8 @@
|
|
|
25
35
|
</template>
|
|
26
36
|
|
|
27
37
|
<script lang="ts" setup>
|
|
28
|
-
import {computed, ref, onUnmounted, watch} from "vue";
|
|
38
|
+
import {computed, ref, onUnmounted, watch, provide} from "vue";
|
|
39
|
+
import {useRouter} from "vue-router";
|
|
29
40
|
import {computedAsync} from "@vueuse/core";
|
|
30
41
|
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
31
42
|
import _ from "lodash";
|
|
@@ -34,19 +45,28 @@
|
|
|
34
45
|
import type * as t from "@/logic/types";
|
|
35
46
|
import * as e from "@/logic/enums";
|
|
36
47
|
import * as tagsFilterState from "@/logic/tagsFilterState";
|
|
48
|
+
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
|
|
37
51
|
const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
|
|
38
52
|
|
|
53
|
+
provide("tagsStates", tagsStates);
|
|
54
|
+
|
|
39
55
|
const globalSettings = useGlobalSettingsStore();
|
|
40
56
|
|
|
41
57
|
globalSettings.mainPanelMode = e.MainPanelMode.Rules;
|
|
42
58
|
|
|
59
|
+
function goToNews() {
|
|
60
|
+
router.push({name: e.MainPanelMode.Entries, params: {}});
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
const rules = computedAsync(async () => {
|
|
44
64
|
// force refresh
|
|
45
65
|
globalSettings.dataVersion;
|
|
46
66
|
return await api.getRules();
|
|
47
67
|
}, null);
|
|
48
68
|
|
|
49
|
-
const
|
|
69
|
+
const tagsCount = computed(() => {
|
|
50
70
|
if (!rules.value) {
|
|
51
71
|
return {};
|
|
52
72
|
}
|
|
@@ -54,7 +74,11 @@
|
|
|
54
74
|
const tags: {[key: string]: number} = {};
|
|
55
75
|
|
|
56
76
|
for (const rule of rules.value) {
|
|
57
|
-
for (const tag of rule.
|
|
77
|
+
for (const tag of rule.requiredTags) {
|
|
78
|
+
tags[tag] = (tags[tag] || 0) + 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const tag of rule.excludedTags) {
|
|
58
82
|
tags[tag] = (tags[tag] || 0) + 1;
|
|
59
83
|
}
|
|
60
84
|
}
|
|
@@ -69,7 +93,7 @@
|
|
|
69
93
|
|
|
70
94
|
let sorted = rules.value.slice();
|
|
71
95
|
|
|
72
|
-
sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.
|
|
96
|
+
sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.requiredTags.concat(rule.excludedTags));
|
|
73
97
|
|
|
74
98
|
const orderProperties = e.RulesOrderProperties.get(globalSettings.rulesOrder);
|
|
75
99
|
|
|
@@ -86,14 +110,14 @@
|
|
|
86
110
|
|
|
87
111
|
sorted = sorted.sort((a: t.Rule, b: t.Rule) => {
|
|
88
112
|
if (globalSettings.rulesOrder === e.RulesOrder.Tags) {
|
|
89
|
-
return utils.compareLexicographically(a.
|
|
113
|
+
return utils.compareLexicographically(a.allTags, b.allTags);
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
const valueA = _.get(a, orderField, null);
|
|
93
117
|
const valueB = _.get(b, orderField, null);
|
|
94
118
|
|
|
95
119
|
if (valueA === null && valueB === null) {
|
|
96
|
-
return utils.compareLexicographically(a.
|
|
120
|
+
return utils.compareLexicographically(a.allTags, b.allTags);
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
if (valueA === null) {
|
|
@@ -112,13 +136,9 @@
|
|
|
112
136
|
return -1 * direction;
|
|
113
137
|
}
|
|
114
138
|
|
|
115
|
-
return utils.compareLexicographically(a.
|
|
139
|
+
return utils.compareLexicographically(a.allTags, b.allTags);
|
|
116
140
|
});
|
|
117
141
|
|
|
118
142
|
return sorted;
|
|
119
143
|
});
|
|
120
|
-
|
|
121
|
-
function onTagStateChanged({tag, state}: {tag: string; state: tagsFilterState.State}) {
|
|
122
|
-
tagsStates.value.onTagStateChanged({tag, state});
|
|
123
|
-
}
|
|
124
144
|
</script>
|