CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / integration (push) Successful in 34s
CI & Build / Python tests (push) Failing after 54s
CI & Build / Build & push image (push) Skipped
The derive queue said the biggest duplicate families were whole views: TaskViewerView, UserManagementView and LogsView were imported nowhere — left behind when tasks moved to the editor and users/logs became SettingsView tabs. Deleted (rule 22). Note/TaskEditorView carried six identical scoped rules -> editor-shared.css (the .tag-suggest-row gap the scoped copies actually rendered wins). Three views wrapped the page under three names -> .page-container in components.css. Three scoped input recipes -> the design system fs-input recipe (snippet #2336), verbatim, in components.css; width/ box-sizing stay with the caller. The three rules panes share .pane and the pane heading via rules-shared.css (the auth-shared pattern, #2852). Ledger: a single-declaration CSS rule keeps its selector in its fingerprint, so `color: var(--fs-text-tertiary)` under five different names is no longer a five-file "identical body" family — the first pay-down found that most of the 148 dup families were exactly this, and nobody would consolidate them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74 lines
1.6 KiB
Vue
74 lines
1.6 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch } from "vue";
|
|
import { apiGet } from "@/api/client";
|
|
|
|
interface Milestone {
|
|
id: number;
|
|
title: string;
|
|
status: string;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
projectId: number | null;
|
|
modelValue: number | null;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: "update:modelValue", val: number | null): void;
|
|
}>();
|
|
|
|
const milestones = ref<Milestone[]>([]);
|
|
const loading = ref(false);
|
|
|
|
watch(
|
|
() => props.projectId,
|
|
async (pid) => {
|
|
milestones.value = [];
|
|
if (!pid) {
|
|
emit("update:modelValue", null);
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
try {
|
|
const data = await apiGet<{ milestones: Milestone[] }>(
|
|
`/api/projects/${pid}/milestones`
|
|
);
|
|
milestones.value = data.milestones.filter((m) => m.status === "active");
|
|
} catch {
|
|
milestones.value = [];
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
function onChange(e: Event) {
|
|
const val = (e.target as HTMLSelectElement).value;
|
|
emit("update:modelValue", val ? Number(val) : null);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<select
|
|
class="fs-input milestone-select"
|
|
:value="modelValue ?? ''"
|
|
:disabled="!projectId || loading"
|
|
@change="onChange"
|
|
>
|
|
<option value="">{{ projectId ? "No milestone" : "(Select project first)" }}</option>
|
|
<option v-for="m in milestones" :key="m.id" :value="m.id">
|
|
{{ m.title }}
|
|
</option>
|
|
</select>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* The input itself is the .fs-input canon (components.css); only the
|
|
layout remainder lives here. */
|
|
.milestone-select {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
}
|
|
</style>
|