Skip to content

Proposals

Design options — multiple variants of one project. Accessed via snaptrude.core.proposals.

A proposal owns a set of buckets (a bucket == a drawing layer); switching the active proposal shows/hides the buckets it owns. Reads never throw (a miss returns null / [] / false) and degrade to empty on legacy / no-proposal projects. Writes are gated by the plugin write floor and are non-undoable (they persist directly, bypassing core.history); create is additionally Pro-gated.

Types

PluginCoreProposalRef

A safe projection of a proposal (the raw redux state is never exposed).

PropertyTypeDescription
idstringProposal id
namestringDisplay name
isActivebooleanWhether this is the active proposal
defaultBucketIdstringThe proposal's default bucket id
bucketIdsstring[]Buckets owned by the proposal
createdAtstringCreation timestamp
createdBystringId of the user who created it

Functions

list()

List every proposal in the project.

  • Returns: { proposals: PluginCoreProposalRef[] }
ts
const { proposals } = await snaptrude.core.proposals.list();

get(proposalId)

Get one proposal by id.

  • Parameters:
    • proposalId: string — The proposal id
  • Returns: PluginCoreProposalRef | nullnull when no proposal matches

getActive()

Get the currently active proposal (honours the present-mode capture override).

  • Returns: PluginCoreProposalRef | nullnull on a legacy / no-proposal project

listForComponent(component)

List the proposals a component belongs to (cross-proposal — buckets can be shared).

  • Parameters:
  • Returns: { proposals: PluginCoreProposalRef[] } — empty when the component is unknown

isActive(proposalId)

Whether a proposal id is the active one.

  • Parameters:
    • proposalId: string — The proposal id
  • Returns: boolean

create(name, options?)

Create a new proposal — blank, or configured from a base proposal (the New Proposal modal's two paths). Pro-gated and write-gated; non-undoable. The name is truncated to 24 chars; empty/duplicate names throw.

Blank (no options) — a bare proposal with a single empty default bucket. Nothing is carried forward and the active proposal does not change.

Configured (options.baseProposalId) — the modal's Configure path: carries the base proposal's buckets (layers) forward per options.layers. Each entry names a bucket of the base proposal with an action — "share" (the new proposal references the same live bucket) or "duplicate" (an independent copy of the bucket and its contents). Buckets not listed are left behind; omitting layers duplicates every base bucket (the modal's default). The base proposal's views and area targets are copied either way, and the new proposal becomes the active one (implicit switch — blocked with PROPOSAL_SWITCH_BLOCKED while an in-canvas agent run is in flight, like setActive).

  • Parameters:
    • name: string — The new proposal's name
    • options: object (optional) — Configure path: { baseProposalId: string, layers?: { bucketId: string, action: "share" | "duplicate" }[] }. layers requires baseProposalId; every bucketId must belong to the base proposal
  • Returns: { id: string } — The created proposal id
  • Throws: when writes are disabled, the workspace is not on a Pro plan, the name is empty/duplicate, layers is passed without baseProposalId, the base proposal is unknown, a listed bucket does not belong to the base proposal (or repeats), or (configured only) PROPOSAL_SWITCH_BLOCKED while a canvas-agent run is in flight
ts
// blank
const { id } = await snaptrude.core.proposals.create("Option B");

// configured: duplicate every layer of the active proposal
const base = await snaptrude.core.proposals.getActive();
const { id: dup } = await snaptrude.core.proposals.create("Option C", {
  baseProposalId: base!.id
});

// configured: share one layer, leave the rest behind
const { id: shared } = await snaptrude.core.proposals.create("Option D", {
  baseProposalId: base!.id,
  layers: [{ bucketId: base!.defaultBucketId, action: "share" }]
});

rename(proposalId, name)

Rename a proposal. Write-gated; non-undoable.

  • Parameters:
    • proposalId: string — The proposal to rename
    • name: string — The new name (truncated to 24 chars)
  • Returns: PluginCoreProposalRef — The renamed proposal
  • Throws: for an unknown id or an empty/duplicate name

setActive(proposalId)

Switch the active proposal — shows/hides the buckets it owns. After the switch, writes are guarded to the new active proposal (edits targeting components outside it are rejected), but list reads are NOT proposal-filtereddesign.query.list* and similar reads stay project-wide; filter their results via listForComponent if you need proposal-scoped sets. Write-gated; non-undoable.

  • Parameters:
    • proposalId: string — The proposal to activate
  • Returns: { id: string } — The now-active proposal id
  • Throws: for an unknown id, or PROPOSAL_SWITCH_BLOCKED when an in-canvas agent run is in flight
ts
await snaptrude.core.proposals.setActive(id);

delete(proposalId, options)

Delete a proposal. DANGEROUS AND IRREVERSIBLE — not undoable. You must choose exactly one explicit disposition (mirroring the product's delete dialog):

  • { deleteObjects: true } — delete the proposal AND permanently destroy the geometry in its exclusive buckets. Buckets shared with other proposals are never destroyed.
  • { targetProposalId } — transfer all of this proposal's buckets to another proposal first (keeping the geometry alive), then delete the emptied proposal — "merge and remove". The target must be a different proposal.

A bare delete(proposalId) is rejected: there is no safe default — without a disposition the engine would drop the proposal's bucket records while leaving their components orphaned. Passing both dispositions at once is also rejected as ambiguous.

Server-validated (a collaborator active in the proposal, or a validation timeout, refuses the delete). The last remaining proposal cannot be deleted. If the proposal is active, the host switches away first. Write-gated.

  • Parameters:
    • proposalId: string — The proposal to delete
    • options: object — Exactly one disposition: { deleteObjects: true } (destroy exclusive-bucket geometry) or { targetProposalId: string } (transfer buckets there, then delete; must differ from proposalId)
  • Returns: { deleted: true }
  • Throws: if no disposition (or both) is given, targetProposalId equals proposalId, the id is unknown, it is the last remaining proposal, or a refused/timed-out server validation
ts
// destroy the proposal and its exclusive geometry
await snaptrude.core.proposals.delete("prop_2", { deleteObjects: true });
// …or keep the geometry by transferring its buckets to prop_1 first
await snaptrude.core.proposals.delete("prop_2", { targetProposalId: "prop_1" });