PhoenixKitProjects.PortalLinks (PhoenixKitProjects v0.21.0)

Copy Markdown View Source

Makes a PUBLISHED portal issue mentionable — as its public URL.

A portal issue IS an assignment, so it shares its uuid with the project_task type. The handler callback receives a flat uuid list with no type attached, so one module answering for both would resolve the same uuid twice and the merge would decide which link won — silently, and in favour of whichever branch ran last. Splitting the type onto its own module is what keeps "the admin page for staff" and "the public page for everyone" from collapsing into each other.

project_task resolves to /admin/projects/list/<project>, which an anonymous reader cannot open. Linking a portal issue to that page would render every # on a public board as a locked chip pointing at an admin URL. This resolves to /portal/<slug>/i/<uuid> — the page the reader is already on the same board as.

Visibility is the board's, not the project's

ResourceLinks.visible_resource_uuids/2 answers "may this viewer open the project", which is the wrong question here: portal readers are usually anonymous and belong to nothing. The right question is "is this issue published on a board this viewer may reach", and that is exactly what Portal.resolve/2 plus the board's own scoping already decide — so this defers to them rather than reimplementing the rule.

Summary

Functions

Titles and RAW public paths for published portal issues.

Not globally searchable, on purpose.

The single type this module owns, for the resource_links/0 callback.

Which of these issues this viewer may actually read.

Functions

resolve_comment_resources(uuids)

@spec resolve_comment_resources([String.t()]) :: map()

Titles and RAW public paths for published portal issues.

Raw because ResourceLinks applies the url prefix once at render. A uuid that is not a published issue on a live portal simply isn't in the result — the caller renders that as missing, which is the correct answer for an issue that was unpublished after someone linked it.

search_resources(query, opts)

@spec search_resources(
  String.t(),
  keyword()
) :: []

Not globally searchable, on purpose.

Core asks every registered type for # results, and a portal issue offered from an ADMIN composer would be the wrong record to link: staff writing internally want the task, which project_task already provides, and a public URL pasted into an internal note invites exactly the mixup this module exists to prevent. The portal composer builds its own list, scoped to the one board being read — see Portal.mention_candidates/4.

types()

@spec types() :: %{required(String.t()) => module()}

The single type this module owns, for the resource_links/0 callback.

visible_resource_uuids(uuids, opts)

@spec visible_resource_uuids(
  [String.t()],
  keyword()
) :: [String.t()]

Which of these issues this viewer may actually read.

Deliberately re-runs the board check per issue rather than trusting that a published row is universally readable: link and members boards publish too, and their audiences are not the open web. Portal.resolve/2 is the same doorway the page itself uses, so a board that goes private, rotates its slug or loses the extension takes its mentions with it.