CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Failing after 39s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Failing after 56s
CI & Build / Build & push image (push) Skipped
Five validators failed on 0e10f6b; these are the ones the logs named. COLLECTION died first and hid everything else: test_inception.py still imported project_rulebook_exclusions, so pytest aborted before running a single test in either the unit or the integration lane. The migrations therefore never ran, which means 0099 and 0100 are still unverified — this push is what puts them in front of real Postgres. The obsolete table test went with the import, and its module docstring now says why rather than just describing one fewer thing. TypeScript: RulebookDetailPane's currentRulebook computed existed only to feed the always-on toggle, so removing the toggle left it unread; the vue 'computed' import went with it. Plugin version minted — plugin content changed and the manifest gates the executing cache (#2209), so the hook check fails until it moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ
141 lines
4.7 KiB
Vue
141 lines
4.7 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, watch } from "vue";
|
|
import { useRulebooksStore } from "@/stores/rulebooks";
|
|
import { apiGet } from "@/api/client";
|
|
import {
|
|
subscribeProject, unsubscribeProject, getProjectApplicableRules,
|
|
} from "@/api/rulebooks";
|
|
import type { RulebookTopic } from "@/api/rulebooks";
|
|
|
|
const props = defineProps<{
|
|
rulebookId: number;
|
|
topics: RulebookTopic[];
|
|
selectedTopicId: number | null;
|
|
}>();
|
|
const emit = defineEmits<{ "select-topic": [id: number] }>();
|
|
|
|
const store = useRulebooksStore();
|
|
const isCreating = ref(false);
|
|
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() {
|
|
const title = newTitle.value.trim();
|
|
if (!title) return;
|
|
const topic = await store.createTopic(props.rulebookId, { title });
|
|
newTitle.value = "";
|
|
isCreating.value = false;
|
|
emit("select-topic", topic.id);
|
|
}
|
|
|
|
onMounted(loadProjects);
|
|
watch(() => props.rulebookId, () => {/* re-render of isSubscribed from existing map */});
|
|
</script>
|
|
|
|
<template>
|
|
<section class="pane">
|
|
<header>
|
|
<h2>Topics</h2>
|
|
</header>
|
|
<ul>
|
|
<li
|
|
v-for="t in topics"
|
|
:key="t.id"
|
|
:class="{ active: t.id === selectedTopicId }"
|
|
@click="emit('select-topic', t.id)"
|
|
>
|
|
{{ t.title }}
|
|
</li>
|
|
</ul>
|
|
<div class="new-topic">
|
|
<button v-if="!isCreating" @click="isCreating = true">+ New topic</button>
|
|
<form v-else @submit.prevent="submitNew">
|
|
<input v-model="newTitle" autofocus placeholder="Topic title (e.g. git-workflow)" />
|
|
<div class="form-buttons">
|
|
<button type="submit">Create</button>
|
|
<button type="button" @click="isCreating = false">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="subscriptions">
|
|
<h3>Subscribers</h3>
|
|
<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>
|
|
</section>
|
|
</template>
|
|
|
|
<style src="@/assets/rules-shared.css" />
|
|
<style scoped>
|
|
header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
ul { list-style: none; padding: 0; margin: 1rem 0; }
|
|
li { padding: 0.5rem; cursor: pointer; border-radius: 6px; }
|
|
li.active { background: var(--fs-accent-soft); }
|
|
li:hover { background: var(--fs-surface-hover); }
|
|
/* `.new-topic` and `.sub-list` are deliberately bare (#2444). The first wraps a
|
|
button-or-form whose children style themselves; the second is a `<ul>`, and
|
|
the bare `ul` rule above already gives it list-style, padding and margin —
|
|
a base a class-name check cannot see, since it comes from an element
|
|
selector. Both namespace descendant rules and assume nothing about layout. */
|
|
.new-topic input {
|
|
width: 100%; margin-bottom: 0.5rem;
|
|
background: var(--fs-surface-page); color: inherit;
|
|
border: 1px solid var(--fs-border-color); border-radius: 6px;
|
|
padding: 0.5rem;
|
|
}
|
|
.subscriptions {
|
|
margin-top: 2rem;
|
|
border-top: 1px solid var(--fs-border-color);
|
|
padding-top: 1rem;
|
|
}
|
|
.subscriptions h3 { font-size: 0.9em; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.sub-list li { cursor: default; }
|
|
.sub-list label { display: flex; gap: 0.5rem; align-items: center; cursor: pointer; }
|
|
button { cursor: pointer; }
|
|
</style>
|