-
Notifications
You must be signed in to change notification settings - Fork 185
feat: org projects pagination and add archived pagination #2312
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
base: main
Are you sure you want to change the base?
Conversation
ConsoleProject ID: Sites (2)
Note You can use Avatars API to generate QR code for any text or URLs. |
WalkthroughAdds client-side pagination for archived projects in src/lib/components/archiveProject.svelte using a Paginator (6 items per page) and adjusts default name truncation from 19 to 16. In src/routes/(console)/organization-[organization]/+page.svelte, the page now prefers data.activeProjectsPage and data.archivedProjectsPage and binds UI totals to activeTotalOverall. In src/routes/(console)/organization-[organization]/+page.ts, the load function aggregates multiple project pages, assigns a default region when missing, splits allProjects into active and archived sets, slices activeProjectsForPage for the current page, and returns activeProjectsPage, archivedProjectsPage, and activeTotalOverall while replacing projects.projects with allProjects and projects.total with the active count. Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 2
🧹 Nitpick comments (10)
src/routes/(console)/organization-[organization]/+page.svelte (3)
107-109
: Avoid redundant filtering when server already provides archived setUse the pre-filtered
data.archivedProjectsPage
directly; fall back to filtering only when it’s absent.- $: projectsToArchive = (data.archivedProjectsPage ?? data.projects.projects).filter( - (project) => project.status !== 'active' - ); + $: projectsToArchive = data.archivedProjectsPage + ?? data.projects.projects.filter((project) => project.status !== 'active');
111-115
: Prefer activeTotal fallback order and align all consumers to it
- Reorder fallbacks so we prefer
data.projects.total
beforedata.organization.projects.length
(the latter may include archived).- Also update any plan checks and props (e.g., Line 140 condition and Line 261 prop to CreateProjectCloud) to use
activeTotalOverall
for consistency.- $: activeProjects = (data.activeProjectsPage ?? data.projects.projects).filter( - (project) => project.status === 'active' - ); - $: activeTotalOverall = data?.activeTotalOverall ?? data?.organization?.projects?.length ?? data?.projects?.total ?? 0; + $: activeProjects = data.activeProjectsPage + ?? data.projects.projects.filter((project) => project.status === 'active'); + $: activeTotalOverall = + data?.activeTotalOverall + ?? data?.projects?.total + ?? data?.organization?.projects?.length + ?? 0;
1-1
: Fix Prettier issuesCI flagged a Prettier formatting warning for this file. Please run:
pnpm prettier --write src/routes/(console)/organization-[organization]/+page.svelte
.src/lib/components/archiveProject.svelte (3)
149-151
: Name truncation tightened to 16 chars — confirm UX parityActive projects use 19/25 (small/regular). Dropping archived to 16 may create inconsistent card widths. Confirm with design or align to active logic.
-function formatName(name: string, limit: number = 16) { +function formatName(name: string, limit: number = 19) { // or make it responsive like the active list
163-168
: Paginator config: hide UI affordances when not needed and avoid magic numbers
- With ≤6 items, you already hide the footer; consider also hiding page numbers.
- Extract
6
into a local constant for clarity and future tuning.- <Paginator - items={projectsToArchive} - limit={6} - hidePages={false} - hideFooter={projectsToArchive.length <= 6}> + {#key projectsToArchive}<!-- reset page when data changes --> + <Paginator + items={projectsToArchive} + limit={ARCHIVE_PAGE_SIZE} + hidePages={projectsToArchive.length <= ARCHIVE_PAGE_SIZE} + hideFooter={projectsToArchive.length <= ARCHIVE_PAGE_SIZE}> {#snippet children(items)} <CardContainer disableEmpty={true} total={projectsToArchive.length}> {#each items as project} ... </CardContainer> {/snippet} </Paginator>Add near the script top:
const ARCHIVE_PAGE_SIZE = 6;Also applies to: 169-171, 264-267
1-1
: Fix Prettier issuesCI flagged a Prettier formatting warning for this file. Please run:
pnpm prettier --write src/lib/components/archiveProject.svelte
.src/routes/(console)/organization-[organization]/+page.ts (4)
56-56
: Set region before deriving slices (minor)Move region normalization before computing
allActiveProjects
/allArchivedProjects
to keep derived arrays fully normalized. Low impact.- // set `default` if no region! - for (const project of allProjects) { + // set `default` if no region! + for (const project of allProjects) { project.region ??= 'default'; } - const allActiveProjects = allProjects.filter((p) => p.status === 'active'); - const allArchivedProjects = allProjects.filter((p) => p.status !== 'active'); + const allActiveProjects = allProjects.filter((p) => p.status === 'active'); + const allArchivedProjects = allProjects.filter((p) => p.status !== 'active');
31-48
: Reduce N+1 API calls: decouple aggregation page size from UI page sizeLooping with
Query.limit(limit)
ties server fetch size to UI page size. Use a larger server-side chunk (e.g., 100) within API limits to cut requests.- const limit = getLimit(url, route, CARD_LIMIT); + const limit = getLimit(url, route, CARD_LIMIT); + const AGG_LIMIT = Math.min(100, limit); // tune to SDK/API max page size ... - Query.limit(limit), + Query.limit(AGG_LIMIT),
1-1
: Fix Prettier issuesCI flagged a Prettier formatting warning for this file. Please run:
pnpm prettier --write src/routes/(console)/organization-[organization]/+page.ts
.
37-42
: Use explicit sort field or document defaultEmpty string in
Query.orderDesc('')
is used across the codebase to invoke the SDK’s default descending order (typically by creation date). For clarity, replace''
with an explicit field (e.g.'$createdAt'
) or add a comment noting that an empty string triggers default sorting.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/lib/components/archiveProject.svelte
(4 hunks)src/routes/(console)/organization-[organization]/+page.svelte
(3 hunks)src/routes/(console)/organization-[organization]/+page.ts
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/routes/(console)/organization-[organization]/+page.ts (2)
src/routes/(console)/organization-[organization]/store.ts (1)
projects
(17-17)src/lib/stores/sdk.ts (1)
sdk
(142-165)
🪛 GitHub Actions: Tests
src/routes/(console)/organization-[organization]/+page.svelte
[warning] 1-1: Prettier formatting issue detected. Run 'prettier --write' to fix.
src/routes/(console)/organization-[organization]/+page.ts
[warning] 1-1: Prettier formatting issue detected. Run 'prettier --write' to fix.
src/lib/components/archiveProject.svelte
[warning] 1-1: Prettier formatting issue detected. Run 'prettier --write' to fix.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: e2e
🔇 Additional comments (5)
src/routes/(console)/organization-[organization]/+page.svelte (2)
165-165
: LGTM: CardContainer now bound to active totalUsing
activeTotalOverall
ensures paging/total reflects active projects only.
250-250
: LGTM: Pagination total matches active total
PaginationWithLimit.total={activeTotalOverall}
is correct and consistent with the active list.src/lib/components/archiveProject.svelte (2)
3-3
: LGTM: Paginator importImporting
Paginator
from$lib/components
is consistent with the component library usage.
258-261
: Safe access on region already handledUsing
region?.name
avoids runtime errors when regions aren’t loaded yet. Good.src/routes/(console)/organization-[organization]/+page.ts (1)
50-54
: LGTM: Server-side split of active vs archived and per-page sliceThis matches the UI’s new bindings and avoids filtering everything on the client.
let allProjects: typeof projects.projects = []; | ||
let fetchedCount = 0; | ||
const total = projects.total; | ||
|
||
while (fetchedCount < total) { | ||
const next = await sdk.forConsole.projects.list({ | ||
queries: [ | ||
Query.offset(fetchedCount), | ||
Query.equal('teamId', params.organization), | ||
Query.limit(limit), | ||
Query.orderDesc('') | ||
], | ||
search: search || undefined | ||
}); | ||
allProjects = allProjects.concat(next.projects); | ||
fetchedCount += next.projects.length; | ||
if (next.projects.length === 0) break; | ||
} |
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.
🛠️ Refactor suggestion
Seed aggregation with the first response and avoid redundant page-0 fetch
You drop the first call’s results and refetch from offset 0, doubling cost and latency. Seed allProjects
with the initial page and continue from there.
- let allProjects: typeof projects.projects = [];
- let fetchedCount = 0;
+ let allProjects: typeof projects.projects = projects.projects.slice();
+ let fetchedCount = allProjects.length;
const total = projects.total;
- while (fetchedCount < total) {
+ while (fetchedCount < total) {
const next = await sdk.forConsole.projects.list({
queries: [
- Query.offset(fetchedCount),
+ Query.offset(fetchedCount),
Query.equal('teamId', params.organization),
- Query.limit(limit),
+ Query.limit(limit),
Query.orderDesc('')
],
search: search || undefined
});
allProjects = allProjects.concat(next.projects);
fetchedCount += next.projects.length;
if (next.projects.length === 0) break;
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
let allProjects: typeof projects.projects = []; | |
let fetchedCount = 0; | |
const total = projects.total; | |
while (fetchedCount < total) { | |
const next = await sdk.forConsole.projects.list({ | |
queries: [ | |
Query.offset(fetchedCount), | |
Query.equal('teamId', params.organization), | |
Query.limit(limit), | |
Query.orderDesc('') | |
], | |
search: search || undefined | |
}); | |
allProjects = allProjects.concat(next.projects); | |
fetchedCount += next.projects.length; | |
if (next.projects.length === 0) break; | |
} | |
// Seed with the initial page of projects instead of starting empty | |
let allProjects: typeof projects.projects = projects.projects.slice(); | |
let fetchedCount = allProjects.length; | |
const total = projects.total; | |
while (fetchedCount < total) { | |
const next = await sdk.forConsole.projects.list({ | |
queries: [ | |
Query.offset(fetchedCount), | |
Query.equal('teamId', params.organization), | |
Query.limit(limit), | |
Query.orderDesc('') | |
], | |
search: search || undefined | |
}); | |
allProjects = allProjects.concat(next.projects); | |
fetchedCount += next.projects.length; | |
if (next.projects.length === 0) break; | |
} |
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/+page.ts around lines 31 to
48, the code currently performs an initial projects fetch but then starts the
loop that refetches from offset 0, doubling cost and latency; instead, perform
the first sdk.forConsole.projects.list call once, initialize allProjects with
that response and set fetchedCount to that response.projects.length, then enter
a loop that requests subsequent pages starting at the current fetchedCount
(using Query.offset(fetchedCount)) until fetchedCount >= total or a page returns
zero items; this removes the redundant page-0 fetch while preserving the same
paging logic.
projects: { ...projects, projects: allProjects, total: allActiveProjects.length }, | ||
activeProjectsPage: activeProjectsForPage, | ||
archivedProjectsPage: allArchivedProjects, | ||
activeTotalOverall: allActiveProjects.length, | ||
search |
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.
💡 Verification agent
🧩 Analysis chain
Don’t mutate the projects
contract; return aggregates separately
Overwriting projects.projects
with all items and projects.total
with “active total” breaks consumers expecting API semantics. Keep projects
intact and expose new top-level fields for UI.
- projects: { ...projects, projects: allProjects, total: allActiveProjects.length },
- activeProjectsPage: activeProjectsForPage,
- archivedProjectsPage: allArchivedProjects,
- activeTotalOverall: allActiveProjects.length,
+ projects, // preserve API contract for stores and other consumers
+ activeProjectsPage: activeProjectsForPage,
+ archivedProjectsPage: allArchivedProjects,
+ activeTotalOverall: allActiveProjects.length,
+ // Optionally expose allProjects if needed elsewhere:
+ // allProjects,
🏁 Script executed:
#!/bin/bash
# Audit for consumers that assume `projects.total` is "all results" (not just active).
rg -nP --type=ts --type=svelte -C2 '\bprojects\.total\b'
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Audit for consumers that assume `projects.total` remains the full count
rg -nP -g '*.ts' -g '*.svelte' -C2 '\bprojects\.total\b'
Length of output: 4397
Preserve projects
API contract and expose aggregates separately
Overriding projects.projects
and projects.total
with active-only values breaks UI components that rely on the full project count (plan checks, creation modals, settings pages). Undo the mutation and introduce new top-level fields for the aggregates, for example:
- projects: { ...projects, projects: allProjects, total: allActiveProjects.length },
+ projects, // preserve API contract
activeProjectsPage: activeProjectsForPage,
archivedProjectsPage: allArchivedProjects,
+ activeTotalOverall: allActiveProjects.length,
+ allProjects, // full list if needed
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/+page.ts around lines 63 to
67, the code is mutating the existing projects object by replacing
projects.projects and projects.total with active-only values which breaks
consumers expecting the full projects API; revert that mutation so the original
projects object remains intact and add new top-level fields (e.g.,
activeProjectsPage and activeProjectsTotal or activeProjectsAggregate) to expose
active-only lists and counts, update any callers that need the active-only
values to use these new fields, and ensure the returned payload preserves the
original projects structure while exposing the aggregates separately.
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.
Actionable comments posted: 2
♻️ Duplicate comments (2)
src/routes/(console)/organization-[organization]/+page.ts (2)
31-48
: Avoid double-fetching and reduce total requests while aggregating.
You re-query from offset 0 after already fetching one page; on page 1 this duplicates page-0, and on later pages you still pay an extra request. Seed aggregation or restructure to a single aggregation pass.Example approach (parallelized, larger page size):
- let allProjects: typeof projects.projects = []; - let fetchedCount = 0; - const total = projects.total; - - while (fetchedCount < total) { - const next = await sdk.forConsole.projects.list({ - queries: [ - Query.offset(fetchedCount), - Query.equal('teamId', params.organization), - Query.limit(limit), - Query.orderDesc('') - ], - search: search || undefined - }); - allProjects = allProjects.concat(next.projects); - fetchedCount += next.projects.length; - if (next.projects.length === 0) break; - } + const total = projects.total; + const pageSize = Math.max(limit, 100); // fewer round-trips + const fetches: Promise<typeof projects>[] = []; + for (let start = 0; start < total; start += pageSize) { + fetches.push( + sdk.forConsole.projects.list({ + queries: [ + Query.offset(start), + Query.equal('teamId', params.organization), + Query.limit(pageSize), + Query.orderDesc('') + ], + search: search || undefined + }) + ); + } + const pages = await Promise.all(fetches); + let allProjects: typeof projects.projects = pages.flatMap((p) => p.projects);
63-66
: Preserve theprojects
API contract; expose aggregates separately.
Overwritingprojects.projects
andprojects.total
breaks consumers that expect server semantics.- projects: { ...projects, projects: allProjects, total: allActiveProjects.length }, - activeProjectsPage: activeProjectsForPage, - archivedProjectsPage: allArchivedProjects, - activeTotalOverall: allActiveProjects.length, + projects, // keep original paging contract + allProjects, // optional: full list for local consumers + activeProjectsPage: activeProjectsForPage, + archivedProjectsPage: allArchivedProjects, + activeTotalOverall: allActiveProjects.length,
🧹 Nitpick comments (4)
src/lib/components/archiveProject.svelte (2)
171-171
: Key your each blocks for stable DOM reconciliation.
Use project id and platform name as keys.- {#each items as project} + {#each items as project (project.$id)} ... - {#each platforms.slice(0, 2) as platform} + {#each platforms.slice(0, 2) as platform (platform.name)}Also applies to: 250-258
166-167
: Nit:hidePages={false}
is redundant ifhideFooter
hides controls.
You can removehidePages
unless it toggles other UI.- hidePages={false} hideFooter={projectsToArchive.length <= 6}>
src/routes/(console)/organization-[organization]/+page.ts (2)
56-58
: Avoid mutating SDK objects in-place when normalizing region.
Safer to return normalized copies to prevent accidental shared-state mutations.- for (const project of allProjects) { - project.region ??= 'default'; - } + allProjects = allProjects.map((p) => ({ ...p, region: p.region ?? 'default' }));
31-48
: Optional: cap concurrency to balance latency and load.
If you parallelize page fetches, use a small pool (e.g., 4–6) rather thanPromise.all
on hundreds of pages.If helpful, I can provide a pooled fetch helper.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/lib/components/archiveProject.svelte
(3 hunks)src/routes/(console)/organization-[organization]/+page.svelte
(3 hunks)src/routes/(console)/organization-[organization]/+page.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/routes/(console)/organization-[organization]/+page.svelte
🔇 Additional comments (3)
src/lib/components/archiveProject.svelte (2)
3-3
: Good addition: local pagination wrapper imported.
Importing Paginator here aligns with the new archived pagination flow.
149-151
: Name truncation tightened (19 → 16): verify UI doesn’t regress.
Please sanity-check long archived names across viewports for truncation/tooltip behavior.src/routes/(console)/organization-[organization]/+page.ts (1)
53-54
: Confirm page semantics when slicing only active projects.
offset
is derived from all projects, but you sliceallActiveProjects
. If archived/active are interleaved, active pages may shift. Ensure the route’spage
is intended to page active-only results.
{@const platforms = filterPlatforms( | ||
project.platforms.map((platform) => | ||
getPlatformInfo(platform.type) | ||
) | ||
)} | ||
{@const formatted = formatName(project.name)} |
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.
Guard against undefined platforms to avoid runtime errors.
Projects without platforms will throw at project.platforms.map(...)
.
Apply:
- {@const platforms = filterPlatforms(
- project.platforms.map((platform) =>
- getPlatformInfo(platform.type)
- )
- )}
+ {@const platforms = filterPlatforms(
+ (project.platforms ?? []).map((platform) =>
+ getPlatformInfo(platform.type)
+ )
+ )}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
{@const platforms = filterPlatforms( | |
project.platforms.map((platform) => | |
getPlatformInfo(platform.type) | |
) | |
)} | |
{@const formatted = formatName(project.name)} | |
{@const platforms = filterPlatforms( | |
(project.platforms ?? []).map((platform) => | |
getPlatformInfo(platform.type) | |
) | |
)} | |
{@const formatted = formatName(project.name)} |
🤖 Prompt for AI Agents
In src/lib/components/archiveProject.svelte around lines 171 to 176, the code
calls project.platforms.map(...) which will throw when project.platforms is
undefined; guard against that by treating platforms as an array (e.g. use
project.platforms?.map(...) ?? [] or Array.isArray(project.platforms) ?
project.platforms.map(...) : []) before passing to filterPlatforms so the
component never calls .map on undefined; update the {@const platforms = ...}
line to use this safe fallback.
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.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/lib/components/archiveProject.svelte (1)
171-176
: Guard against undefined platforms to avoid runtime errors.
This still calls.map
on a possibly undefinedproject.platforms
. Use a safe fallback.- {@const platforms = filterPlatforms( - project.platforms.map((platform) => - getPlatformInfo(platform.type) - ) - )} + {@const platforms = filterPlatforms( + (project.platforms ?? []).map((platform) => + getPlatformInfo(platform.type) + ) + )}
🧹 Nitpick comments (3)
src/lib/components/archiveProject.svelte (3)
149-151
: Truncation default lowered to 16 — confirm UX consistency.
Verify this aligns with design across breakpoints; otherwise consider keeping the previous limit or making it a shared constant.
163-170
: Paginator wiring and CardContainer total — confirm contract.
- Ensure Paginator supports the
{#snippet children(items)}
API withitems
as the paged slice and thathideFooter
behaves as intended.- If CardContainer’s
total
is meant to reflect items on the current page (not overall), switch toitems.length
to avoid UI/skeleton mismatches.Proposed change if per-page is expected:
- <CardContainer disableEmpty={true} total={projectsToArchive.length}> + <CardContainer disableEmpty={true} total={items.length}>
170-170
: Key the each block for stable DOM when paginating.
Helps avoid unnecessary re-renders when page content changes.- {#each items as project} + {#each items as project (project.$id)}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/lib/components/archiveProject.svelte
(3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: e2e
- GitHub Check: build
🔇 Additional comments (3)
src/lib/components/archiveProject.svelte (3)
3-3
: Importing Paginator for client-side pagination looks good.
260-265
: Off-by-one fix for “+N” badge — LGTM.
Condition> 2
correctly shows “+1” when there are 3 platforms.
268-271
: Region rendering gated safely — LGTM.
The$regionsStore?.regions
check prevents.find
on undefined and the UI handles missing names.
let fetchedCount = 0; | ||
const total = projects.total; | ||
|
||
while (fetchedCount < total) { |
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.
I think we should add to query by status and do separate query for active and archived projects,
if (next.projects.length === 0) break; | ||
} | ||
|
||
const allActiveProjects = allProjects.filter((p) => p.status === 'active'); |
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.
also, if status !== archived, then it's active project, currently status can be null, active or archived, where both null and active means active
What does this PR do?
Test Plan
(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)
Related PRs and Issues
(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)
Have you read the Contributing Guidelines on issues?
yes
Summary by CodeRabbit
New Features
Bug Fixes
Style