feat(500): Settings shows the shipped reply shapes, what adjusts each, and how often each arrives
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 1m58s
CI & Build / Build & push image (push) Successful in 39s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 1m58s
CI & Build / Build & push image (push) Successful in 39s
Settings > General > Reply shapes. One menu row per shape (core, completion, asks, plan): its title and when it arrives, how many of the operator's preferences adjust it, and its deliveries over 30 days (in full / as a reminder, "—" when the counts cannot be read). A verdict line leads. Opening a row shows the shipped text, read-only because it is product, and the preferences mounted on its moment; each opens in the rule editor, and "Adjust this" opens a new preference already mounted on the shape's moment with a starting trigger. The rule editor takes an optional preset (kind, moments, trigger) and, opened outside the Rules view, asks where a new record lives instead of dropping it. GET /api/retrieval/reply-shapes now returns reply_shapes.overview: the catalog plus mounted preferences (the same lookup that delivers them) and counts from the reply_shape delivery rows. #5497 (step 5 of milestone 500). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,14 @@ import { useCanonicalSystemsStore } from "@/stores/canonicalSystems";
|
||||
import { useMomentsStore } from "@/stores/moments";
|
||||
import RuleHistoryPanel from "@/components/rules/RuleHistoryPanel.vue";
|
||||
import RuleHomePicker from "@/components/rules/RuleHomePicker.vue";
|
||||
import type { Rule, RuleKind } from "@/api/rulebooks";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
import { listRulebooks, listTopics, type Rule, type RuleKind, type RulePreset } from "@/api/rulebooks";
|
||||
|
||||
const props = defineProps<{ ruleId: number | null; topicId: number | null }>();
|
||||
const props = defineProps<{
|
||||
ruleId: number | null;
|
||||
topicId: number | null;
|
||||
preset?: RulePreset | null;
|
||||
}>();
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
const store = useRulebooksStore();
|
||||
@@ -128,10 +133,10 @@ async function load() {
|
||||
} else {
|
||||
title.value = "";
|
||||
statement.value = "";
|
||||
whenToApply.value = "";
|
||||
kind.value = "rule";
|
||||
whenToApply.value = props.preset?.whenToApply ?? "";
|
||||
kind.value = props.preset?.kind ?? "rule";
|
||||
systemIds.value = [];
|
||||
ruleMoments.value = [];
|
||||
ruleMoments.value = [...(props.preset?.moments ?? [])];
|
||||
why.value = "";
|
||||
howToApply.value = "";
|
||||
verifyWith.value = "";
|
||||
@@ -139,7 +144,31 @@ async function load() {
|
||||
}
|
||||
procedureDraft.value = "";
|
||||
procedureError.value = "";
|
||||
await Promise.all([canon.fetchCatalog(), momentsStore.load()]);
|
||||
await Promise.all([canon.fetchCatalog(), momentsStore.load(), loadHomes()]);
|
||||
}
|
||||
|
||||
// ── Where a new record lives, when the opener did not say ──────────────────
|
||||
// The rules view opens the editor inside a topic; Settings does not, so a new
|
||||
// record opened there asks for its home. A rulebook topic is global — it
|
||||
// applies in every project, as a reply shape does.
|
||||
const homeChoices = ref<{ id: number; label: string }[]>([]);
|
||||
const chosenTopic = ref<number | null>(null);
|
||||
const homeError = ref("");
|
||||
const needsHome = computed(() => isCreating.value && props.topicId === null);
|
||||
|
||||
async function loadHomes() {
|
||||
if (!needsHome.value || homeChoices.value.length) return;
|
||||
try {
|
||||
const books = await listRulebooks();
|
||||
const perBook = await Promise.all(books.map(async (rb) => {
|
||||
const ts = await listTopics(rb.id);
|
||||
return ts.map((t) => ({ id: t.id, label: `${rb.title} › ${t.title}` }));
|
||||
}));
|
||||
homeChoices.value = perBook.flat();
|
||||
if (homeChoices.value.length === 1) chosenTopic.value = homeChoices.value[0].id;
|
||||
} catch (e: unknown) {
|
||||
homeError.value = apiErrorMessage(e, "Could not load where this could live");
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@@ -166,8 +195,14 @@ async function save() {
|
||||
verify_with: verifyWith.value,
|
||||
expires_when: expiresWhen.value,
|
||||
};
|
||||
if (isCreating.value && props.topicId !== null) {
|
||||
await store.createRule(props.topicId, fields);
|
||||
const home = props.topicId ?? chosenTopic.value;
|
||||
if (isCreating.value && home === null) {
|
||||
// Written but homeless: keep the draft open rather than lose it on close.
|
||||
homeError.value = "Choose where it lives before saving.";
|
||||
return;
|
||||
}
|
||||
if (isCreating.value && home !== null) {
|
||||
await store.createRule(home, fields);
|
||||
} else if (props.ruleId !== null) {
|
||||
await store.updateRule(props.ruleId, fields);
|
||||
}
|
||||
@@ -201,6 +236,14 @@ watch(() => props.ruleId, load);
|
||||
<button v-if="!isCreating" class="trash" @click="remove" aria-label="Delete">🗑</button>
|
||||
<button class="close" @click="save" aria-label="Close">×</button>
|
||||
</header>
|
||||
<label v-if="needsHome">
|
||||
Where it lives
|
||||
<select v-model="chosenTopic" class="fs-input">
|
||||
<option :value="null" disabled>Choose a rulebook topic</option>
|
||||
<option v-for="h in homeChoices" :key="h.id" :value="h.id">{{ h.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<p v-if="homeError" class="home-error" role="alert">{{ homeError }}</p>
|
||||
<label>
|
||||
Title
|
||||
<input v-model="title" placeholder="e.g. dev is home" />
|
||||
@@ -421,6 +464,7 @@ watch(() => props.ruleId, load);
|
||||
one index everywhere (tests/test_frontend_shared_styles.py). -->
|
||||
<style src="@/assets/rules-shared.css" />
|
||||
<style scoped>
|
||||
.home-error { margin: 0 0 var(--fs-space-2); color: var(--fs-error); font-size: var(--fs-size-body-sm); }
|
||||
.kind { border: 1px solid var(--fs-border-color); border-radius: var(--fs-radius-md); padding: var(--fs-space-3); margin: var(--fs-space-3) 0; }
|
||||
.kind legend { font-size: var(--fs-size-tiny); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); color: var(--fs-text-tertiary); padding: 0 var(--fs-space-2); }
|
||||
.kind-opt { display: flex; align-items: flex-start; gap: var(--fs-space-2); margin-bottom: var(--fs-space-2); font-size: var(--fs-size-body-sm); line-height: var(--fs-leading-body); }
|
||||
|
||||
Reference in New Issue
Block a user