feat(rulebook): subscription panel — toggle projects per rulebook
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref, onMounted, watch } from "vue";
|
||||||
import { useRulebooksStore } from "@/stores/rulebooks";
|
import { useRulebooksStore } from "@/stores/rulebooks";
|
||||||
|
import { apiGet } from "@/api/client";
|
||||||
|
import {
|
||||||
|
subscribeProject, unsubscribeProject, getProjectApplicableRules,
|
||||||
|
} from "@/api/rulebooks";
|
||||||
import type { RulebookTopic } from "@/api/rulebooks";
|
import type { RulebookTopic } from "@/api/rulebooks";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -14,6 +18,41 @@ const store = useRulebooksStore();
|
|||||||
const isCreating = ref(false);
|
const isCreating = ref(false);
|
||||||
const newTitle = ref("");
|
const newTitle = ref("");
|
||||||
|
|
||||||
|
interface ProjectLite { id: number; title: string }
|
||||||
|
const projects = ref<ProjectLite[]>([]);
|
||||||
|
// Map<project_id, Set<rulebook_id>>
|
||||||
|
const subscribedRulebookIds = ref<Map<number, Set<number>>>(new Map());
|
||||||
|
|
||||||
|
async function loadProjects() {
|
||||||
|
const data = await apiGet<{ projects: ProjectLite[] }>("/api/projects");
|
||||||
|
projects.value = data.projects;
|
||||||
|
for (const p of projects.value) {
|
||||||
|
const result = await getProjectApplicableRules(p.id);
|
||||||
|
subscribedRulebookIds.value.set(
|
||||||
|
p.id,
|
||||||
|
new Set(result.subscribed_rulebooks.map((rb) => rb.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSubscribed(projectId: number): boolean {
|
||||||
|
return subscribedRulebookIds.value.get(projectId)?.has(props.rulebookId) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleSubscription(projectId: number, checked: boolean) {
|
||||||
|
if (checked) {
|
||||||
|
await subscribeProject(projectId, props.rulebookId);
|
||||||
|
const set = subscribedRulebookIds.value.get(projectId) || new Set<number>();
|
||||||
|
set.add(props.rulebookId);
|
||||||
|
subscribedRulebookIds.value.set(projectId, set);
|
||||||
|
} else {
|
||||||
|
await unsubscribeProject(projectId, props.rulebookId);
|
||||||
|
subscribedRulebookIds.value.get(projectId)?.delete(props.rulebookId);
|
||||||
|
}
|
||||||
|
// trigger reactivity on Map mutation
|
||||||
|
subscribedRulebookIds.value = new Map(subscribedRulebookIds.value);
|
||||||
|
}
|
||||||
|
|
||||||
async function submitNew() {
|
async function submitNew() {
|
||||||
const title = newTitle.value.trim();
|
const title = newTitle.value.trim();
|
||||||
if (!title) return;
|
if (!title) return;
|
||||||
@@ -22,6 +61,9 @@ async function submitNew() {
|
|||||||
isCreating.value = false;
|
isCreating.value = false;
|
||||||
emit("select-topic", topic.id);
|
emit("select-topic", topic.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(loadProjects);
|
||||||
|
watch(() => props.rulebookId, () => {/* re-render of isSubscribed from existing map */});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -47,10 +89,20 @@ async function submitNew() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- Subscription panel — populated in T14. -->
|
|
||||||
<div class="subscriptions">
|
<div class="subscriptions">
|
||||||
<h3>Subscribers</h3>
|
<h3>Subscribers</h3>
|
||||||
<p class="hint">(subscription management — populated in next task)</p>
|
<ul class="sub-list">
|
||||||
|
<li v-for="p in projects" :key="p.id">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="isSubscribed(p.id)"
|
||||||
|
@change="toggleSubscription(p.id, ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
{{ p.title }}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -75,6 +127,7 @@ li:hover { background: var(--color-hover, rgba(255,255,255,0.05)); }
|
|||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
.subscriptions h3 { font-size: 0.9em; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.05em; }
|
.subscriptions h3 { font-size: 0.9em; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
.hint { font-size: 0.85em; opacity: 0.6; }
|
.sub-list li { cursor: default; }
|
||||||
|
.sub-list label { display: flex; gap: 0.5rem; align-items: center; cursor: pointer; }
|
||||||
button { cursor: pointer; }
|
button { cursor: pointer; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user