+ class="w-5 h-5 align-text-bottom mx-1 inline" />
@@ -25,16 +27,17 @@
{{ purifiedTitle }}
@@ -70,7 +73,7 @@
diff --git a/site/src/components/FfunTag.vue b/site/src/components/FfunTag.vue
index 3e99fd12..a64e8f3c 100644
--- a/site/src/components/FfunTag.vue
+++ b/site/src/components/FfunTag.vue
@@ -1,35 +1,52 @@
-
-
[{{ count }}]
-
- {{ tagInfo.name }}
-
+
- ↗
-
+ href="#"
+ v-if="showSwitch"
+ class="pr-1"
+ @click.prevent="onRevers()"
+ >⇄
+
+
[{{ count }}]
+
+ {{ tagInfo.name }}
+
+
+ ↗
+
+
diff --git a/site/src/components/RuleForList.vue b/site/src/components/RuleForList.vue
index 35075660..c4ff4a10 100644
--- a/site/src/components/RuleForList.vue
+++ b/site/src/components/RuleForList.vue
@@ -19,9 +19,19 @@
-
+
+
+
+
+
@@ -67,7 +77,8 @@
await api.updateRule({
id: properties.rule.id,
score: newScore,
- tags: properties.rule.tags
+ requiredTags: properties.rule.requiredTags,
+ excludedTags: properties.rule.excludedTags
});
scoreChanged.value = true;
diff --git a/site/src/components/TagsFilter.vue b/site/src/components/TagsFilter.vue
index d9c1b334..48da4015 100644
--- a/site/src/components/TagsFilter.vue
+++ b/site/src/components/TagsFilter.vue
@@ -7,26 +7,21 @@
v-for="tag of displayedSelectedTags"
:key="tag"
class="whitespace-nowrap line-clamp-1">
- [X]
-
+ :showSwitch="true"
+ count-mode="no" />
+
+
+ count-mode="prefix" />
@@ -57,18 +50,24 @@
diff --git a/site/src/components/TagsList.vue b/site/src/components/TagsList.vue
index 72057c29..17f46de4 100644
--- a/site/src/components/TagsList.vue
+++ b/site/src/components/TagsList.vue
@@ -1,72 +1,53 @@
-
+
+ :secondary-mode="tagMode(tag)"
+ count-mode="tooltip" />
{{ tagsNumber - showLimit }} more
-
-
hide 0"
+ >[{{ tagsNumber - showLimit }} more]
-
-
diff --git a/site/src/components/notifications/Block.vue b/site/src/components/notifications/Block.vue
index 2a90cc1b..4681da64 100644
--- a/site/src/components/notifications/Block.vue
+++ b/site/src/components/notifications/Block.vue
@@ -6,9 +6,11 @@
diff --git a/site/src/layouts/SidePanelLayout.vue b/site/src/layouts/SidePanelLayout.vue
index f1fb14e7..08b487c3 100644
--- a/site/src/layouts/SidePanelLayout.vue
+++ b/site/src/layouts/SidePanelLayout.vue
@@ -36,7 +36,7 @@
v-if="reloadButton"
href="#"
@click="globalSettings.updateDataVersion()"
- >ReloadRefresh
diff --git a/site/src/logic/api.ts b/site/src/logic/api.ts
index d9cc875f..566542a4 100644
--- a/site/src/logic/api.ts
+++ b/site/src/logic/api.ts
@@ -99,10 +99,22 @@ export async function getEntriesByIds({ids}: {ids: t.EntryId[]}) {
return entries;
}
-export async function createOrUpdateRule({tags, score}: {tags: string[]; score: number}) {
+export async function createOrUpdateRule({
+ requiredTags,
+ excludedTags,
+ score
+}: {
+ requiredTags: string[];
+ excludedTags: string[];
+ score: number;
+}) {
const response = await post({
url: API_CREATE_OR_UPDATE_RULE,
- data: {tags: tags, score: score}
+ data: {
+ requiredTags: requiredTags,
+ excludedTags: excludedTags,
+ score: score
+ }
});
return response;
}
@@ -112,10 +124,20 @@ export async function deleteRule({id}: {id: t.RuleId}) {
return response;
}
-export async function updateRule({id, tags, score}: {id: t.RuleId; tags: string[]; score: number}) {
+export async function updateRule({
+ id,
+ requiredTags,
+ excludedTags,
+ score
+}: {
+ id: t.RuleId;
+ requiredTags: string[];
+ excludedTags: string[];
+ score: number;
+}) {
const response = await post({
url: API_UPDATE_RULE,
- data: {id: id, tags: tags, score: score}
+ data: {id: id, score: score, requiredTags: requiredTags, excludedTags: excludedTags}
});
return response;
}
diff --git a/site/src/logic/asserts.ts b/site/src/logic/asserts.ts
new file mode 100644
index 00000000..74fb1349
--- /dev/null
+++ b/site/src/logic/asserts.ts
@@ -0,0 +1,5 @@
+export function defined
(value: T | null | undefined, name?: string): asserts value is T {
+ if (value === null || value === undefined) {
+ throw new Error(`${name ?? "Value"} is null or undefined`);
+ }
+}
diff --git a/site/src/logic/tagsFilterState.ts b/site/src/logic/tagsFilterState.ts
index 3c17da3c..422b211f 100644
--- a/site/src/logic/tagsFilterState.ts
+++ b/site/src/logic/tagsFilterState.ts
@@ -1,3 +1,6 @@
+import {ref, computed, reactive} from "vue";
+import type {ComputedRef} from "vue";
+
export type State = "required" | "excluded" | "none";
interface ReturnTagsForEntity {
@@ -7,48 +10,97 @@ interface ReturnTagsForEntity {
export class Storage {
requiredTags: {[key: string]: boolean};
excludedTags: {[key: string]: boolean};
+ selectedTags: ComputedRef<{[key: string]: boolean}>;
+ hasSelectedTags: ComputedRef;
constructor() {
- this.requiredTags = {};
- this.excludedTags = {};
+ this.requiredTags = reactive({});
+ this.excludedTags = reactive({});
+
+ this.selectedTags = computed(() => {
+ return {...this.requiredTags, ...this.excludedTags};
+ });
+
+ this.hasSelectedTags = computed(() => {
+ return Object.keys(this.selectedTags.value).length > 0;
+ });
}
onTagStateChanged({tag, state}: {tag: string; state: State}) {
if (state === "required") {
this.requiredTags[tag] = true;
- this.excludedTags[tag] = false;
+ if (this.excludedTags[tag]) {
+ delete this.excludedTags[tag];
+ }
} else if (state === "excluded") {
this.excludedTags[tag] = true;
- this.requiredTags[tag] = false;
+ if (this.requiredTags[tag]) {
+ delete this.requiredTags[tag];
+ }
} else if (state === "none") {
- this.excludedTags[tag] = false;
- this.requiredTags[tag] = false;
+ if (this.requiredTags[tag]) {
+ delete this.requiredTags[tag];
+ }
+
+ if (this.excludedTags[tag]) {
+ delete this.excludedTags[tag];
+ }
} else {
throw new Error(`Unknown tag state: ${state}`);
}
}
+ onTagReversed({tag}: {tag: string}) {
+ if (!(tag in this.selectedTags)) {
+ this.onTagStateChanged({tag: tag, state: "required"});
+ } else if (this.requiredTags[tag]) {
+ this.onTagStateChanged({tag: tag, state: "excluded"});
+ } else if (this.excludedTags[tag]) {
+ this.onTagStateChanged({tag: tag, state: "required"});
+ } else {
+ throw new Error(`Unknown tag state: ${tag}`);
+ }
+ }
+
+ onTagClicked({tag}: {tag: string}) {
+ if (tag in this.selectedTags) {
+ this.onTagStateChanged({tag: tag, state: "none"});
+ } else {
+ this.onTagStateChanged({tag: tag, state: "required"});
+ }
+ }
+
filterByTags(entities: any[], getTags: ReturnTagsForEntity) {
let report = entities.slice();
+ const requiredTags = Object.keys(this.requiredTags);
+
report = report.filter((entity) => {
for (const tag of getTags(entity)) {
if (this.excludedTags[tag]) {
return false;
}
}
- return true;
- });
- report = report.filter((entity) => {
- for (const tag of Object.keys(this.requiredTags)) {
- if (this.requiredTags[tag] && !getTags(entity).includes(tag)) {
+ for (const tag of requiredTags) {
+ if (!getTags(entity).includes(tag)) {
return false;
}
}
+
return true;
});
return report;
}
+
+ clear() {
+ Object.keys(this.requiredTags).forEach((key) => {
+ delete this.requiredTags[key];
+ });
+
+ Object.keys(this.excludedTags).forEach((key) => {
+ delete this.excludedTags[key];
+ });
+ }
}
diff --git a/site/src/logic/types.ts b/site/src/logic/types.ts
index 6ffa086c..f1e34847 100644
--- a/site/src/logic/types.ts
+++ b/site/src/logic/types.ts
@@ -231,7 +231,9 @@ export function entryFromJSON(
export type Rule = {
readonly id: RuleId;
- readonly tags: string[];
+ readonly requiredTags: string[];
+ readonly excludedTags: string[];
+ readonly allTags: string[];
readonly score: number;
readonly createdAt: Date;
readonly updatedAt: Date;
@@ -239,22 +241,27 @@ export type Rule = {
export function ruleFromJSON({
id,
- tags,
+ requiredTags,
+ excludedTags,
score,
createdAt,
updatedAt
}: {
id: string;
- tags: string[];
+ requiredTags: string[];
+ excludedTags: string[];
score: number;
createdAt: string;
updatedAt: string;
}): Rule {
- tags = tags.sort();
+ requiredTags = requiredTags.sort();
+ excludedTags = excludedTags.sort();
return {
id: toRuleId(id),
- tags: tags,
+ requiredTags: requiredTags,
+ excludedTags: excludedTags,
+ allTags: requiredTags.concat(excludedTags),
score: score,
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt)
diff --git a/site/src/stores/entries.ts b/site/src/stores/entries.ts
index 65513e0a..15ca1c9b 100644
--- a/site/src/stores/entries.ts
+++ b/site/src/stores/entries.ts
@@ -8,12 +8,14 @@ import * as api from "@/logic/api";
import {Timer} from "@/logic/timer";
import {computedAsync} from "@vueuse/core";
import {useGlobalSettingsStore} from "@/stores/globalSettings";
+import * as events from "@/logic/events";
export const useEntriesStore = defineStore("entriesStore", () => {
const globalSettings = useGlobalSettingsStore();
const entries = ref<{[key: t.EntryId]: t.Entry}>({});
const requestedEntries = ref<{[key: t.EntryId]: boolean}>({});
+ const displayedEntryId = ref(null);
function registerEntry(entry: t.Entry) {
if (entry.id in entries.value) {
@@ -95,11 +97,35 @@ export const useEntriesStore = defineStore("entriesStore", () => {
}
}
+ async function displayEntry({entryId}: {entryId: t.EntryId}) {
+ displayedEntryId.value = entryId;
+
+ requestFullEntry({entryId: entryId});
+
+ if (!entries.value[entryId].hasMarker(e.Marker.Read)) {
+ await setMarker({
+ entryId: entryId,
+ marker: e.Marker.Read
+ });
+ }
+
+ await events.newsBodyOpened({entryId: entryId});
+ }
+
+ function hideEntry({entryId}: {entryId: t.EntryId}) {
+ if (displayedEntryId.value === entryId) {
+ displayedEntryId.value = null;
+ }
+ }
+
return {
entries,
requestFullEntry,
setMarker,
removeMarker,
- loadedEntriesReport
+ loadedEntriesReport,
+ displayedEntryId,
+ displayEntry,
+ hideEntry
};
});
diff --git a/site/src/stores/globalSettings.ts b/site/src/stores/globalSettings.ts
index b373151c..265e5b58 100644
--- a/site/src/stores/globalSettings.ts
+++ b/site/src/stores/globalSettings.ts
@@ -18,7 +18,6 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
// Entries
const lastEntriesPeriod = ref(e.LastEntriesPeriod.Day3);
const entriesOrder = ref(e.EntriesOrder.Score);
- const showEntriesTags = ref(true);
const showRead = ref(true);
// Feeds
@@ -64,7 +63,6 @@ export const useGlobalSettingsStore = defineStore("globalSettings", () => {
mainPanelMode,
lastEntriesPeriod,
entriesOrder,
- showEntriesTags,
showRead,
dataVersion,
updateDataVersion,
diff --git a/site/src/values/Score.vue b/site/src/values/Score.vue
index 770afbad..10f8eeb1 100644
--- a/site/src/values/Score.vue
+++ b/site/src/values/Score.vue
@@ -30,7 +30,7 @@
for (const rule of rules) {
const tags = [];
- for (const tagId of rule.tags) {
+ for (const tagId of rule.requiredTags) {
const tagInfo = tagsStore.tags[tagId];
if (tagInfo) {
tags.push(tagInfo.name);
@@ -39,7 +39,16 @@
}
}
- strings.push(rule.score.toString().padStart(2, " ") + " — " + tags.join(", "));
+ for (const tagId of rule.excludedTags) {
+ const tagInfo = tagsStore.tags[tagId];
+ if (tagInfo) {
+ tags.push("NOT " + tagInfo.name);
+ } else {
+ tags.push("NOT " + tagId);
+ }
+ }
+
+ strings.push(rule.score.toString().padStart(2, " ") + " — " + tags.join(" AND "));
}
alert(strings.join("\n"));
diff --git a/site/src/views/NewsView.vue b/site/src/views/NewsView.vue
index 27b54a65..708a1fe4 100644
--- a/site/src/views/NewsView.vue
+++ b/site/src/views/NewsView.vue
@@ -15,15 +15,6 @@
- Show tags:
-
-
-
-
Show read:
+ :show-create-rule="true" />
@@ -55,16 +46,14 @@
+ :showPerPage="25" />
diff --git a/site/src/views/RulesView.vue b/site/src/views/RulesView.vue
index cc8fcccf..f3afcdd3 100644
--- a/site/src/views/RulesView.vue
+++ b/site/src/views/RulesView.vue
@@ -13,11 +13,21 @@
-
+
+
+
You can create new rules on the
+ news
+ tab.
+
+
@@ -25,7 +35,8 @@