-
Notifications
You must be signed in to change notification settings - Fork 13
feat: dashboard needs-attention counts and lifecycle activation emails #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
17f6068
2c35b1b
525944d
185025f
89dc412
4835006
777d764
4f04102
94adc71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,10 @@ | ||
| # Only fail npm audit on high/critical severity vulnerabilities. | ||
| audit-level=high | ||
|
|
||
| # npm resolves peer dependencies differently than every lockfile this project | ||
| # has ever produced: the lock was generated on machines with | ||
| # legacy-peer-deps=true in ~/.npmrc, so CI (which has no user npmrc) rejected | ||
| # it with "Missing: oxc-parser@0.144.0 from lock file" — an optional peer of | ||
| # unctx that legacy resolution skips. Setting it here makes the resolution | ||
| # mode a property of the repo rather than of whoever last ran npm install. | ||
| legacy-peer-deps=true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,18 +147,48 @@ watch(activeStatus, (newStatus) => { | |
| }) | ||
|
|
||
| const statusFilter = computed(() => activeStatus.value) | ||
|
|
||
| // ── Viewed filter ───────────────────────────────────────────────────────────── | ||
| // Server-side read receipts, per signed-in user. The dashboard's "To Review" | ||
| // tile links here with `?viewed=unviewed`, so the count it shows and the rows | ||
| // listed here come from the same definition. | ||
|
|
||
| const VIEWED_OPTIONS = ['unviewed', 'viewed'] as const | ||
| type ViewedFilter = typeof VIEWED_OPTIONS[number] | ||
|
|
||
| const initialViewed = VIEWED_OPTIONS.includes(route.query.viewed as any) | ||
| ? (route.query.viewed as ViewedFilter) | ||
| : undefined | ||
| const activeViewed = useState<ViewedFilter | undefined>(`app-filter-viewed${scopeSuffix}`, () => initialViewed) | ||
| if (initialViewed !== undefined) { | ||
| activeViewed.value = initialViewed | ||
| } | ||
|
|
||
| watch(activeViewed, (next) => { | ||
| const query = { ...route.query } | ||
| if (next) { | ||
| query.viewed = next | ||
| } | ||
| else { | ||
| delete query.viewed | ||
| } | ||
| router.replace({ query }) | ||
| }) | ||
|
Comment on lines
+159
to
+176
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C 5 'activeStatus|activeViewed|route\.query\.(status|viewed)|router\.replace|clearAllFilters|applySettings' \
app/components/ApplicationsList.vueRepository: reqcore-inc/reqcore Length of output: 9982 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- component imports and filter setup ---'
sed -n '1,190p' app/components/ApplicationsList.vue
printf '%s\n' '--- settings and view application logic ---'
sed -n '350,445p' app/components/ApplicationsList.vue
printf '%s\n' '--- route/filter references in the component ---'
rg -n -C 4 'useRoute|useRouter|route\.query|watch\(|useState|clearAllFilters|applySettings' app/components/ApplicationsList.vueRepository: reqcore-inc/reqcore Length of output: 15095 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- dependency versions ---'
rg -n '"(vue|vue-router|nuxt)"' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
printf '%s\n' '--- deterministic watcher/query model ---'
python3 - <<'PY'
from copy import deepcopy
def status_write(route_query, status):
query = deepcopy(route_query)
if status:
query["status"] = status
else:
query.pop("status", None)
return query
def viewed_write(route_query, viewed):
query = deepcopy(route_query)
if viewed:
query["viewed"] = viewed
else:
query.pop("viewed", None)
return query
cases = [
("both added", {}, "new", "unviewed"),
("both changed", {"status": "old", "viewed": "viewed"}, "new", "unviewed"),
("both cleared", {"status": "new", "viewed": "unviewed"}, None, None),
]
for name, route, status, viewed in cases:
first = status_write(route, status)
second = viewed_write(route, viewed)
print(name)
print(" status watcher submits:", first)
print(" viewed watcher submits:", second)
print(" last submitted query:", second)
PYRepository: reqcore-inc/reqcore Length of output: 2274 🌐 Web query:
💡 Result: In Vue Router 4, the router.replace method (like router.push) returns a Promise that allows you to wait until the navigation is fully finished [1][2]. Key technical details regarding this Promise: 1. Asynchronous Resolution: The Promise resolves once the navigation successfully completes or fails [3]. Navigation includes the processing of all asynchronous navigation guards, such as those that resolve async route components or perform data fetching [4]. 2. Success vs. Failure: To handle navigation failures (e.g., cancelling a navigation via next(false)), the Promise resolves to either undefined (on success) or a NavigationFailure object (if the navigation was aborted or cancelled) [3]. 3. Rejection: The Promise only rejects if an unhandled error occurs during the navigation process (e.g., an error inside a navigation guard) [3]. In such cases, the error will also be triggered through the router.onError hook [3]. Because the Promise resolves rather than rejects when a navigation is simply cancelled, you can await the call safely without triggering "Uncaught (in promise)" errors for expected navigation failures [3]. By checking the returned value or catching errors specifically, you can differentiate between successful, cancelled, and failed navigations [3]. Citations:
Synchronize status and viewed filters in one route-query update. Watch route changes and update both 🤖 Prompt for AI Agents |
||
|
|
||
| const viewedFilter = computed(() => activeViewed.value) | ||
| const propertyFilters = ref<import('~~/shared/properties').PropertyFilter[]>([]) | ||
| const jobIdFilter = computed(() => props.jobId) | ||
|
|
||
| const { applications, total, fetchStatus, error, refresh } = useApplications({ | ||
| page, | ||
| limit: pageSize, | ||
| status: statusFilter, | ||
| viewed: viewedFilter, | ||
| propertyFilters, | ||
| jobId: jobIdFilter, | ||
| }) | ||
|
|
||
| watch([statusFilter, propertyFilters, pageSize], () => { | ||
| watch([statusFilter, viewedFilter, propertyFilters, pageSize], () => { | ||
| page.value = 1 | ||
| }, { deep: true }) | ||
|
|
||
|
|
@@ -251,11 +281,12 @@ const filteredApplications = computed(() => { | |
| }) | ||
|
|
||
| const hasActiveFilters = computed(() => | ||
| activeStatus.value != null || activeJobId.value != null || debouncedSearch.value.length > 0 || propertyFilters.value.length > 0, | ||
| activeStatus.value != null || activeViewed.value != null || activeJobId.value != null || debouncedSearch.value.length > 0 || propertyFilters.value.length > 0, | ||
| ) | ||
|
|
||
| function clearAllFilters() { | ||
| activeStatus.value = undefined | ||
| activeViewed.value = undefined | ||
| activeJobId.value = undefined | ||
| searchInput.value = '' | ||
| debouncedSearch.value = '' | ||
|
|
@@ -312,6 +343,7 @@ const statusLabels: Record<Status, string> = { | |
|
|
||
| type ApplicationsViewSettings = { | ||
| status?: Status | ||
| viewed?: ViewedFilter | ||
| jobId?: string | ||
| propertyFilters: import('~~/shared/properties').PropertyFilter[] | ||
| sortKey: SortKey | ||
|
|
@@ -321,6 +353,7 @@ type ApplicationsViewSettings = { | |
|
|
||
| const defaultSettings: ApplicationsViewSettings = { | ||
| status: undefined, | ||
| viewed: undefined, | ||
| jobId: undefined, | ||
| propertyFilters: [], | ||
| sortKey: 'created', | ||
|
|
@@ -335,6 +368,7 @@ const isFullscreen = ref(false) | |
| const isWideDetail = ref(!!props.jobId) | ||
| const currentSettings = computed<ApplicationsViewSettings>(() => ({ | ||
| status: activeStatus.value, | ||
| viewed: activeViewed.value, | ||
| jobId: activeJobId.value, | ||
| propertyFilters: [...propertyFilters.value], | ||
| sortKey: sortKey.value, | ||
|
|
@@ -344,6 +378,7 @@ const currentSettings = computed<ApplicationsViewSettings>(() => ({ | |
|
|
||
| function applySettings(s: ApplicationsViewSettings) { | ||
| activeStatus.value = s.status | ||
| activeViewed.value = s.viewed | ||
| activeJobId.value = s.jobId | ||
| propertyFilters.value = [...(s.propertyFilters ?? [])] | ||
| sortKey.value = s.sortKey | ||
|
|
@@ -406,7 +441,7 @@ function onUpdateView(id: string) { | |
| } | ||
|
|
||
| const drawerActiveCount = computed(() => | ||
| [activeStatus.value, activeJobId.value].filter(Boolean).length + propertyFilters.value.length, | ||
| [activeStatus.value, activeViewed.value, activeJobId.value].filter(Boolean).length + propertyFilters.value.length, | ||
| ) | ||
|
|
||
| // ── Property value lookup helper ────────────────────────────────────────────── | ||
|
|
@@ -543,6 +578,40 @@ async function handleApplicationDeleted() { | |
| </div> | ||
| </div> | ||
|
|
||
| <!-- Viewed --> | ||
| <div> | ||
| <label class="block text-xs font-semibold uppercase tracking-wide text-surface-500 dark:text-surface-400 mb-2">Viewed</label> | ||
| <div class="flex flex-wrap gap-1.5"> | ||
| <button | ||
| type="button" | ||
| class="rounded-full px-3 py-1.5 text-xs font-medium transition-colors" | ||
| :class="!activeViewed | ||
| ? 'bg-surface-900 text-white dark:bg-surface-100 dark:text-surface-900' | ||
| : 'bg-surface-100 dark:bg-surface-800 text-surface-500 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-700'" | ||
| @click="activeViewed = undefined" | ||
| >Any</button> | ||
| <button | ||
| type="button" | ||
| class="rounded-full px-3 py-1.5 text-xs font-medium transition-colors" | ||
| :class="activeViewed === 'unviewed' | ||
| ? 'bg-surface-900 text-white dark:bg-surface-100 dark:text-surface-900' | ||
| : 'bg-surface-100 dark:bg-surface-800 text-surface-500 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-700'" | ||
| @click="activeViewed = activeViewed === 'unviewed' ? undefined : 'unviewed'" | ||
| >Not viewed by me</button> | ||
| <button | ||
| type="button" | ||
| class="rounded-full px-3 py-1.5 text-xs font-medium transition-colors" | ||
| :class="activeViewed === 'viewed' | ||
| ? 'bg-surface-900 text-white dark:bg-surface-100 dark:text-surface-900' | ||
| : 'bg-surface-100 dark:bg-surface-800 text-surface-500 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-700'" | ||
| @click="activeViewed = activeViewed === 'viewed' ? undefined : 'viewed'" | ||
| >Viewed by me</button> | ||
| </div> | ||
| <p v-if="activeViewed === 'unviewed'" class="mt-2 text-xs text-surface-400 dark:text-surface-500"> | ||
| Rejected applicants are excluded — they've already been decided on. | ||
| </p> | ||
| </div> | ||
|
|
||
| <!-- Job (global list only) --> | ||
| <div v-if="!jobId"> | ||
| <label class="block text-xs font-semibold uppercase tracking-wide text-surface-500 dark:text-surface-400 mb-2">Job</label> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: reqcore-inc/reqcore
Length of output: 1488
🏁 Script executed:
Repository: reqcore-inc/reqcore
Length of output: 32309
🏁 Script executed:
Repository: reqcore-inc/reqcore
Length of output: 437
Synchronize
detailTabwithinitialTab.When same-application navigation changes
route.query.tab,initialTabupdates butdetailTabdoes not. The watcher at Lines 642-646 observes onlyprops.applicationId. Add a watcher forprops.initialTab.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents