feeds-fun 1.15.0 → 1.16.1
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/EntriesList.vue +5 -10
- package/src/components/EntryForList.vue +20 -39
- package/src/components/FfunTag.vue +55 -30
- 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 +1 -1
- package/src/logic/api.ts +26 -4
- package/src/logic/asserts.ts +5 -0
- package/src/logic/tagsFilterState.ts +63 -11
- package/src/logic/types.ts +12 -5
- package/src/stores/entries.ts +27 -1
- package/src/stores/globalSettings.ts +0 -2
- package/src/values/Score.vue +11 -2
- package/src/views/FeedsView.vue +9 -0
- package/src/views/NewsView.vue +51 -51
- package/src/views/RulesView.vue +34 -14
package/package.json
CHANGED
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
<entry-for-list
|
|
10
10
|
:entryId="entryId"
|
|
11
11
|
:time-field="timeField"
|
|
12
|
-
:
|
|
13
|
-
:tags-count="tagsCount"
|
|
14
|
-
@entry:bodyVisibilityChanged="onBodyVisibilityChanged" />
|
|
12
|
+
:tags-count="tagsCount" />
|
|
15
13
|
</li>
|
|
16
14
|
</ul>
|
|
17
15
|
|
|
@@ -35,14 +33,11 @@
|
|
|
35
33
|
const properties = defineProps<{
|
|
36
34
|
entriesIds: Array<t.EntryId>;
|
|
37
35
|
timeField: string;
|
|
38
|
-
showTags: boolean;
|
|
39
36
|
showFromStart: number;
|
|
40
37
|
showPerPage: number;
|
|
41
38
|
tagsCount: {[key: string]: number};
|
|
42
39
|
}>();
|
|
43
40
|
|
|
44
|
-
const emit = defineEmits(["entry:bodyVisibilityChanged"]);
|
|
45
|
-
|
|
46
41
|
const showEntries = ref(properties.showFromStart);
|
|
47
42
|
|
|
48
43
|
const entriesToShow = computed(() => {
|
|
@@ -51,14 +46,14 @@
|
|
|
51
46
|
}
|
|
52
47
|
return properties.entriesIds.slice(0, showEntries.value);
|
|
53
48
|
});
|
|
54
|
-
|
|
55
|
-
function onBodyVisibilityChanged({entryId, visible}: {entryId: t.EntryId; visible: boolean}) {
|
|
56
|
-
emit("entry:bodyVisibilityChanged", {entryId, visible});
|
|
57
|
-
}
|
|
58
49
|
</script>
|
|
59
50
|
|
|
60
51
|
<style scoped>
|
|
52
|
+
.entry-block {
|
|
53
|
+
}
|
|
54
|
+
|
|
61
55
|
.entry-block:not(:last-child) {
|
|
62
56
|
border-bottom-width: 1px;
|
|
57
|
+
@apply py-1;
|
|
63
58
|
}
|
|
64
59
|
</style>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
ref="entryTop"
|
|
4
|
+
class="flex text-lg">
|
|
3
5
|
<div class="flex-shrink-0 w-8 text-right pr-1">
|
|
4
6
|
<value-score
|
|
5
7
|
:value="entry.score"
|
|
@@ -9,7 +11,7 @@
|
|
|
9
11
|
<div class="flex-shrink-0 w-8 text-right pr-1">
|
|
10
12
|
<favicon-element
|
|
11
13
|
:url="entry.url"
|
|
12
|
-
class="w-
|
|
14
|
+
class="w-5 h-5 align-text-bottom mx-1 inline" />
|
|
13
15
|
</div>
|
|
14
16
|
|
|
15
17
|
<div class="flex-shrink-0 text-right">
|
|
@@ -25,16 +27,17 @@
|
|
|
25
27
|
<a
|
|
26
28
|
:href="entry.url"
|
|
27
29
|
target="_blank"
|
|
28
|
-
:class="[{'font-bold': isRead}, 'flex-grow', 'min-w-fit', 'line-clamp-1', 'pr-4', 'mb-0']"
|
|
30
|
+
:class="[{'font-bold': !isRead}, 'flex-grow', 'min-w-fit', 'line-clamp-1', 'pr-4', 'mb-0']"
|
|
29
31
|
@click="onTitleClick">
|
|
30
32
|
{{ purifiedTitle }}
|
|
31
33
|
</a>
|
|
32
34
|
|
|
33
35
|
<tags-list
|
|
34
|
-
v-if="showTags"
|
|
35
36
|
class="mt-0 pt-0"
|
|
36
37
|
:tags="entry.tags"
|
|
37
38
|
:tags-count="tagsCount"
|
|
39
|
+
:show-all="showBody"
|
|
40
|
+
@request-to-show-all="entriesStore.displayEntry({entryId: entry.id})"
|
|
38
41
|
:contributions="entry.scoreContributions" />
|
|
39
42
|
</div>
|
|
40
43
|
|
|
@@ -70,7 +73,7 @@
|
|
|
70
73
|
|
|
71
74
|
<script lang="ts" setup>
|
|
72
75
|
import _ from "lodash";
|
|
73
|
-
import {computed, ref} from "vue";
|
|
76
|
+
import {computed, ref, useTemplateRef} from "vue";
|
|
74
77
|
import type * as t from "@/logic/types";
|
|
75
78
|
import * as events from "@/logic/events";
|
|
76
79
|
import * as e from "@/logic/enums";
|
|
@@ -80,15 +83,14 @@
|
|
|
80
83
|
|
|
81
84
|
const entriesStore = useEntriesStore();
|
|
82
85
|
|
|
86
|
+
const topElement = useTemplateRef("entryTop");
|
|
87
|
+
|
|
83
88
|
const properties = defineProps<{
|
|
84
89
|
entryId: t.EntryId;
|
|
85
90
|
timeField: string;
|
|
86
|
-
showTags: boolean;
|
|
87
91
|
tagsCount: {[key: string]: number};
|
|
88
92
|
}>();
|
|
89
93
|
|
|
90
|
-
const emit = defineEmits(["entry:bodyVisibilityChanged"]);
|
|
91
|
-
|
|
92
94
|
const entry = computed(() => {
|
|
93
95
|
if (properties.entryId in entriesStore.entries) {
|
|
94
96
|
return entriesStore.entries[properties.entryId];
|
|
@@ -98,10 +100,12 @@
|
|
|
98
100
|
});
|
|
99
101
|
|
|
100
102
|
const isRead = computed(() => {
|
|
101
|
-
return
|
|
103
|
+
return entriesStore.entries[entry.value.id].hasMarker(e.Marker.Read);
|
|
102
104
|
});
|
|
103
105
|
|
|
104
|
-
const showBody =
|
|
106
|
+
const showBody = computed(() => {
|
|
107
|
+
return entry.value.id == entriesStore.displayedEntryId;
|
|
108
|
+
});
|
|
105
109
|
|
|
106
110
|
const timeFor = computed(() => {
|
|
107
111
|
if (entry.value === null) {
|
|
@@ -111,25 +115,6 @@
|
|
|
111
115
|
return _.get(entry.value, properties.timeField, null);
|
|
112
116
|
});
|
|
113
117
|
|
|
114
|
-
async function displayBody() {
|
|
115
|
-
showBody.value = true;
|
|
116
|
-
|
|
117
|
-
emit("entry:bodyVisibilityChanged", {entryId: properties.entryId, visible: true});
|
|
118
|
-
|
|
119
|
-
if (entry.value === null) {
|
|
120
|
-
throw new Error("entry is null");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
entriesStore.requestFullEntry({entryId: entry.value.id});
|
|
124
|
-
|
|
125
|
-
await events.newsBodyOpened({entryId: entry.value.id});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function hideBody() {
|
|
129
|
-
showBody.value = false;
|
|
130
|
-
emit("entry:bodyVisibilityChanged", {entryId: properties.entryId, visible: false});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
118
|
const purifiedTitle = computed(() => {
|
|
134
119
|
if (entry.value === null) {
|
|
135
120
|
return "";
|
|
@@ -166,20 +151,16 @@
|
|
|
166
151
|
event.stopPropagation();
|
|
167
152
|
|
|
168
153
|
if (showBody.value) {
|
|
169
|
-
|
|
154
|
+
entriesStore.hideEntry({entryId: entry.value.id});
|
|
170
155
|
} else {
|
|
171
|
-
|
|
156
|
+
await entriesStore.displayEntry({entryId: entry.value.id});
|
|
157
|
+
|
|
158
|
+
if (topElement.value) {
|
|
159
|
+
topElement.value.scrollIntoView({behavior: "instant"});
|
|
160
|
+
}
|
|
172
161
|
}
|
|
173
162
|
} else {
|
|
174
163
|
await newsLinkOpenedEvent();
|
|
175
164
|
}
|
|
176
|
-
|
|
177
|
-
// TODO: is it will be too slow?
|
|
178
|
-
if (showBody.value) {
|
|
179
|
-
await entriesStore.setMarker({
|
|
180
|
-
entryId: properties.entryId,
|
|
181
|
-
marker: e.Marker.Read
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
165
|
}
|
|
185
166
|
</script>
|
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
:class="classes"
|
|
4
|
-
:title="tooltip"
|
|
5
|
-
@click.prevent="onClick()">
|
|
6
|
-
<span v-if="countMode == 'prefix'">[{{ count }}]</span>
|
|
7
|
-
|
|
8
|
-
{{ tagInfo.name }}
|
|
9
|
-
|
|
2
|
+
<div class="inline-block">
|
|
10
3
|
<a
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@click.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
href="#"
|
|
5
|
+
v-if="showSwitch"
|
|
6
|
+
class="pr-1"
|
|
7
|
+
@click.prevent="onRevers()"
|
|
8
|
+
>⇄</a
|
|
9
|
+
>
|
|
10
|
+
<div
|
|
11
|
+
:class="classes"
|
|
12
|
+
:title="tooltip"
|
|
13
|
+
@click.prevent="onClick()">
|
|
14
|
+
<span v-if="countMode == 'prefix'">[{{ count }}]</span>
|
|
15
|
+
|
|
16
|
+
{{ tagInfo.name }}
|
|
17
|
+
|
|
18
|
+
<a
|
|
19
|
+
v-if="tagInfo.link"
|
|
20
|
+
:href="tagInfo.link"
|
|
21
|
+
target="_blank"
|
|
22
|
+
@click.stop=""
|
|
23
|
+
rel="noopener noreferrer">
|
|
24
|
+
↗
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
18
27
|
</div>
|
|
19
28
|
</template>
|
|
20
29
|
|
|
21
30
|
<script lang="ts" setup>
|
|
22
31
|
import * as t from "@/logic/types";
|
|
23
|
-
import {computed, ref} from "vue";
|
|
32
|
+
import {computed, ref, inject} from "vue";
|
|
33
|
+
import type {Ref} from "vue";
|
|
24
34
|
import {useTagsStore} from "@/stores/tags";
|
|
35
|
+
import * as tagsFilterState from "@/logic/tagsFilterState";
|
|
36
|
+
import * as asserts from "@/logic/asserts";
|
|
25
37
|
|
|
26
38
|
const tagsStore = useTagsStore();
|
|
27
39
|
|
|
40
|
+
const tagsStates = inject<Ref<tagsFilterState.Storage>>("tagsStates");
|
|
41
|
+
|
|
42
|
+
asserts.defined(tagsStates);
|
|
43
|
+
|
|
28
44
|
const properties = defineProps<{
|
|
29
45
|
uid: string;
|
|
30
46
|
count?: number | null;
|
|
31
47
|
countMode?: string | null;
|
|
32
|
-
|
|
48
|
+
secondaryMode?: string | null;
|
|
49
|
+
showSwitch?: boolean | null;
|
|
33
50
|
}>();
|
|
34
51
|
|
|
35
52
|
const tagInfo = computed(() => {
|
|
@@ -44,27 +61,39 @@
|
|
|
44
61
|
return t.noInfoTag(properties.uid);
|
|
45
62
|
});
|
|
46
63
|
|
|
47
|
-
const emit = defineEmits(["tag:clicked"]);
|
|
48
|
-
|
|
49
64
|
const classes = computed(() => {
|
|
50
65
|
const result: {[key: string]: boolean} = {
|
|
51
66
|
tag: true
|
|
52
67
|
};
|
|
53
68
|
|
|
54
|
-
if (properties.
|
|
55
|
-
result[
|
|
69
|
+
if (tagsStates.value.requiredTags[properties.uid]) {
|
|
70
|
+
result["required"] = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (tagsStates.value.excludedTags[properties.uid]) {
|
|
74
|
+
result["excluded"] = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (properties.secondaryMode) {
|
|
78
|
+
result[properties.secondaryMode] = true;
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
return result;
|
|
59
82
|
});
|
|
60
83
|
|
|
61
84
|
function onClick() {
|
|
62
|
-
|
|
85
|
+
asserts.defined(tagsStates);
|
|
86
|
+
tagsStates.value.onTagClicked({tag: properties.uid});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function onRevers() {
|
|
90
|
+
asserts.defined(tagsStates);
|
|
91
|
+
tagsStates.value.onTagReversed({tag: properties.uid});
|
|
63
92
|
}
|
|
64
93
|
|
|
65
94
|
const tooltip = computed(() => {
|
|
66
95
|
if (properties.countMode == "tooltip" && properties.count) {
|
|
67
|
-
return `articles with
|
|
96
|
+
return `articles with this tag: ${properties.count}`;
|
|
68
97
|
}
|
|
69
98
|
return "";
|
|
70
99
|
});
|
|
@@ -72,19 +101,15 @@
|
|
|
72
101
|
|
|
73
102
|
<style scoped>
|
|
74
103
|
.tag {
|
|
75
|
-
@apply inline-block cursor-pointer p-0 mr-2 whitespace-nowrap;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.tag.selected {
|
|
79
|
-
@apply font-bold text-purple-700;
|
|
104
|
+
@apply inline-block cursor-pointer p-0 mr-2 whitespace-nowrap hover:bg-green-100 px-1 hover:rounded-lg;
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
.tag.required {
|
|
83
|
-
@apply text-green-700;
|
|
108
|
+
@apply text-green-700 font-bold;
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
.tag.excluded {
|
|
87
|
-
@apply text-red-700;
|
|
112
|
+
@apply text-red-700 font-bold;
|
|
88
113
|
}
|
|
89
114
|
|
|
90
115
|
.tag.positive {
|
|
@@ -1,42 +1,73 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
<div>
|
|
3
|
+
<div
|
|
4
|
+
v-if="tagsStates.hasSelectedTags"
|
|
5
|
+
class="flex items-center">
|
|
6
|
+
<div class="flex-none">
|
|
7
|
+
<score-selector
|
|
8
|
+
class="inline-block mr-2 my-auto"
|
|
9
|
+
v-model="currentScore" />
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<a
|
|
13
|
+
class="ffun-form-button p-1 my-1 block text-center inline-block flex-grow"
|
|
14
|
+
href="#"
|
|
15
|
+
@click.prevent="createOrUpdateRule()"
|
|
16
|
+
>Create Rule</a
|
|
17
|
+
>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<p
|
|
21
|
+
class="ffun-info-good"
|
|
22
|
+
v-else>
|
|
23
|
+
<template v-if="showSuccess"> Rule created. </template>
|
|
24
|
+
<template v-else> Select tags to create a rule. </template>
|
|
25
|
+
</p>
|
|
16
26
|
</div>
|
|
17
27
|
</template>
|
|
18
28
|
|
|
19
29
|
<script lang="ts" setup>
|
|
20
|
-
import {computed, ref} from "vue";
|
|
21
|
-
import {
|
|
30
|
+
import {computed, ref, inject, watch} from "vue";
|
|
31
|
+
import type {Ref} from "vue";
|
|
32
|
+
import {useTagsStore} from "@/stores/tags";
|
|
33
|
+
import type * as tagsFilterState from "@/logic/tagsFilterState";
|
|
34
|
+
import * as asserts from "@/logic/asserts";
|
|
22
35
|
import * as api from "@/logic/api";
|
|
23
|
-
|
|
36
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
37
|
+
|
|
38
|
+
const tagsStore = useTagsStore();
|
|
24
39
|
|
|
25
40
|
const globalSettings = useGlobalSettingsStore();
|
|
26
41
|
|
|
27
|
-
const
|
|
42
|
+
const currentScore = ref(1);
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
const scores = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610];
|
|
44
|
+
const showSuccess = ref(false);
|
|
31
45
|
|
|
32
|
-
const
|
|
46
|
+
const tagsStates = inject<Ref<tagsFilterState.Storage>>("tagsStates");
|
|
47
|
+
asserts.defined(tagsStates);
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
watch(
|
|
50
|
+
() => tagsStates.value.hasSelectedTags,
|
|
51
|
+
() => {
|
|
52
|
+
// This condition is needed to prevent immediate reset of the success message
|
|
53
|
+
// right after the rule is created in createOrUpdateRule
|
|
54
|
+
if (tagsStates.value.hasSelectedTags) {
|
|
55
|
+
showSuccess.value = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
);
|
|
37
59
|
|
|
38
60
|
async function createOrUpdateRule() {
|
|
39
|
-
|
|
61
|
+
asserts.defined(tagsStates);
|
|
62
|
+
await api.createOrUpdateRule({
|
|
63
|
+
requiredTags: Object.keys(tagsStates.value.requiredTags),
|
|
64
|
+
excludedTags: Object.keys(tagsStates.value.excludedTags),
|
|
65
|
+
score: currentScore.value
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
tagsStates.value.clear();
|
|
69
|
+
|
|
70
|
+
showSuccess.value = true;
|
|
40
71
|
|
|
41
72
|
// this line leads to the reloading of news and any other data
|
|
42
73
|
// not an elegant solution, but it works with the current API implementation
|
|
@@ -44,7 +75,5 @@
|
|
|
44
75
|
// - without reloading
|
|
45
76
|
// - maybe, without reordering too
|
|
46
77
|
globalSettings.updateDataVersion();
|
|
47
|
-
|
|
48
|
-
emit("rule-constructor:created");
|
|
49
78
|
}
|
|
50
79
|
</script>
|
|
@@ -19,9 +19,19 @@
|
|
|
19
19
|
|
|
20
20
|
<div class="flex-grow">
|
|
21
21
|
<template
|
|
22
|
-
v-for="tag of rule.
|
|
22
|
+
v-for="tag of rule.requiredTags"
|
|
23
23
|
:key="tag">
|
|
24
|
-
<ffun-tag
|
|
24
|
+
<ffun-tag
|
|
25
|
+
:uid="tag"
|
|
26
|
+
secondary-mode="positive" />
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<template
|
|
30
|
+
v-for="tag of rule.excludedTags"
|
|
31
|
+
:key="tag">
|
|
32
|
+
<ffun-tag
|
|
33
|
+
:uid="tag"
|
|
34
|
+
secondary-mode="negative" />
|
|
25
35
|
</template>
|
|
26
36
|
</div>
|
|
27
37
|
</div>
|
|
@@ -67,7 +77,8 @@
|
|
|
67
77
|
await api.updateRule({
|
|
68
78
|
id: properties.rule.id,
|
|
69
79
|
score: newScore,
|
|
70
|
-
|
|
80
|
+
requiredTags: properties.rule.requiredTags,
|
|
81
|
+
excludedTags: properties.rule.excludedTags
|
|
71
82
|
});
|
|
72
83
|
|
|
73
84
|
scoreChanged.value = true;
|
|
@@ -7,26 +7,21 @@
|
|
|
7
7
|
v-for="tag of displayedSelectedTags"
|
|
8
8
|
:key="tag"
|
|
9
9
|
class="whitespace-nowrap line-clamp-1">
|
|
10
|
-
<a
|
|
11
|
-
href="#"
|
|
12
|
-
@click.prevent="deselect(tag)"
|
|
13
|
-
>[X]</a
|
|
14
|
-
>
|
|
15
10
|
<ffun-tag
|
|
16
11
|
class="ml-1"
|
|
17
12
|
:uid="tag"
|
|
18
13
|
:count="tags[tag] ?? 0"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@tag:clicked="onTagClicked">
|
|
22
|
-
</ffun-tag>
|
|
14
|
+
:showSwitch="true"
|
|
15
|
+
count-mode="no" />
|
|
23
16
|
</li>
|
|
24
17
|
</ul>
|
|
25
18
|
|
|
19
|
+
<rule-constructor v-if="showCreateRule" />
|
|
20
|
+
|
|
26
21
|
<input
|
|
27
22
|
class="ffun-input w-full"
|
|
28
23
|
type="text"
|
|
29
|
-
placeholder="Input part of tag
|
|
24
|
+
placeholder="Input part of a tag…"
|
|
30
25
|
v-model="tagNameFilter" />
|
|
31
26
|
|
|
32
27
|
<ul
|
|
@@ -39,9 +34,7 @@
|
|
|
39
34
|
<ffun-tag
|
|
40
35
|
:uid="tag"
|
|
41
36
|
:count="tags[tag]"
|
|
42
|
-
count-mode="prefix"
|
|
43
|
-
:mode="null"
|
|
44
|
-
@tag:clicked="onTagClicked" />
|
|
37
|
+
count-mode="prefix" />
|
|
45
38
|
</li>
|
|
46
39
|
</ul>
|
|
47
40
|
|
|
@@ -57,18 +50,24 @@
|
|
|
57
50
|
</template>
|
|
58
51
|
|
|
59
52
|
<script lang="ts" setup>
|
|
60
|
-
import {computed, ref} from "vue";
|
|
53
|
+
import {computed, ref, inject} from "vue";
|
|
54
|
+
import type {Ref} from "vue";
|
|
61
55
|
import {useTagsStore} from "@/stores/tags";
|
|
62
56
|
import type * as tagsFilterState from "@/logic/tagsFilterState";
|
|
57
|
+
import * as asserts from "@/logic/asserts";
|
|
58
|
+
import * as api from "@/logic/api";
|
|
59
|
+
import {useGlobalSettingsStore} from "@/stores/globalSettings";
|
|
60
|
+
|
|
63
61
|
const tagsStore = useTagsStore();
|
|
64
62
|
|
|
65
|
-
const
|
|
63
|
+
const globalSettings = useGlobalSettingsStore();
|
|
66
64
|
|
|
67
|
-
const
|
|
65
|
+
const currentScore = ref(1);
|
|
68
66
|
|
|
69
|
-
const
|
|
67
|
+
const tagsStates = inject<Ref<tagsFilterState.Storage>>("tagsStates");
|
|
68
|
+
asserts.defined(tagsStates);
|
|
70
69
|
|
|
71
|
-
const properties = defineProps<{tags: {[key: string]: number}}>();
|
|
70
|
+
const properties = defineProps<{tags: {[key: string]: number}; showCreateRule?: boolean}>();
|
|
72
71
|
|
|
73
72
|
const showFromStart = ref(25);
|
|
74
73
|
|
|
@@ -76,37 +75,40 @@
|
|
|
76
75
|
|
|
77
76
|
const tagNameFilter = ref("");
|
|
78
77
|
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
const bCount = properties.tags[b];
|
|
78
|
+
function createTagComparator() {
|
|
79
|
+
const counts = properties.tags;
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
function tagComparator(a: string, b: string) {
|
|
82
|
+
const aCount = counts[a];
|
|
83
|
+
const bCount = counts[b];
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
if (aCount > bCount) {
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
if (aCount < bCount) {
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
if (a > b) {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (a < b) {
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return 0;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
return
|
|
104
|
+
return tagComparator;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const displayedSelectedTags = computed(() => {
|
|
103
|
-
let values = Object.keys(
|
|
108
|
+
let values = Object.keys(tagsStates.value.selectedTags);
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
values.sort(tagComparator);
|
|
110
|
+
const comparator = createTagComparator();
|
|
111
|
+
values.sort(comparator);
|
|
110
112
|
|
|
111
113
|
return values;
|
|
112
114
|
});
|
|
@@ -114,22 +116,38 @@
|
|
|
114
116
|
const totalTags = computed(() => {
|
|
115
117
|
// TODO: this is not correct, because selected tags are treated differently
|
|
116
118
|
// depending on their status: required or excluded.
|
|
119
|
+
// I.e. by excluding tag `x` you exclude concreate entries => you can exclude more tags,
|
|
120
|
+
// if they only belong to the excluded entries)
|
|
117
121
|
// => value is not accurate, but it is ok for now
|
|
118
|
-
return Object.keys(properties.tags).length + Object.keys(
|
|
122
|
+
return Object.keys(properties.tags).length + Object.keys(tagsStates.value.selectedTags).length;
|
|
119
123
|
});
|
|
120
124
|
|
|
121
|
-
const
|
|
125
|
+
const _sortedTags = computed(() => {
|
|
122
126
|
let values = Object.keys(properties.tags);
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
return [];
|
|
126
|
-
}
|
|
128
|
+
const comparator = createTagComparator();
|
|
127
129
|
|
|
128
|
-
values
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
values.sort(comparator);
|
|
131
|
+
|
|
132
|
+
return values;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const _visibleTags = computed(() => {
|
|
136
|
+
let values = _sortedTags.value.slice();
|
|
137
|
+
|
|
138
|
+
const textFilter = tagNameFilter.value.trim().toLowerCase();
|
|
139
|
+
|
|
140
|
+
const selectedTags = Object.keys(tagsStates.value.selectedTags);
|
|
131
141
|
|
|
132
142
|
values = values.filter((tag) => {
|
|
143
|
+
if (selectedTags.includes(tag)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!textFilter) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
const tagInfo = tagsStore.tags[tag];
|
|
134
152
|
|
|
135
153
|
if (tagInfo === undefined || tagInfo.name === null) {
|
|
@@ -139,36 +157,10 @@
|
|
|
139
157
|
return tagInfo.name.includes(tagNameFilter.value);
|
|
140
158
|
});
|
|
141
159
|
|
|
142
|
-
values.sort(tagComparator);
|
|
143
|
-
|
|
144
|
-
values = values.slice(0, showEntries.value);
|
|
145
|
-
|
|
146
160
|
return values;
|
|
147
161
|
});
|
|
148
162
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (state === "none") {
|
|
153
|
-
tagStates.value[tag] = "required";
|
|
154
|
-
selectedTags.value[tag] = true;
|
|
155
|
-
} else if (state === "required") {
|
|
156
|
-
tagStates.value[tag] = "excluded";
|
|
157
|
-
selectedTags.value[tag] = true;
|
|
158
|
-
} else if (state === "excluded") {
|
|
159
|
-
tagStates.value[tag] = "required";
|
|
160
|
-
selectedTags.value[tag] = true;
|
|
161
|
-
} else {
|
|
162
|
-
throw new Error(`Unknown tag state: ${state}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
emit("tag:stateChanged", {tag: tag, state: tagStates.value[tag]});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function deselect(tag: string) {
|
|
169
|
-
selectedTags.value[tag] = false;
|
|
170
|
-
tagStates.value[tag] = "none";
|
|
171
|
-
|
|
172
|
-
emit("tag:stateChanged", {tag: tag, state: tagStates.value[tag]});
|
|
173
|
-
}
|
|
163
|
+
const displayedTags = computed(() => {
|
|
164
|
+
return _visibleTags.value.slice(0, showEntries.value);
|
|
165
|
+
});
|
|
174
166
|
</script>
|