The :only_changes filter re-seeked each journal's predecessor in every EXISTS
branch (~7 LATERAL lookups per row). A CTE now shadows the journals table,
exposing predecessor_id/predecessor_data_id once per row, and each branch reads
those columns instead. On a 703-journal work package this cuts the COUNT from
~1.13M to ~35K shared buffers.
https://community.openproject.org/wp/STC-462
Two readability passes over the work package activity tab, no behaviour change. The paginator's private methods are reordered to follow their call order so the file reads top-down from `#call`, and the three activity filter modes (`:all`, `:only_comments`, `:only_changes`) — until now bare symbols duplicated across the controller, paginator, journal components and the hidden form — move into a single `WorkPackages::ActivitiesTab::Filters` module so the modes have one source of truth and can't drift apart. The diff reaches beyond the paginator into the controller, several components and a form, since that's where the symbols were scattered.
The work package activity tab computed a per-journal sequence_version on
every render — a ROW_NUMBER() window function over a LATERAL join — only to
stamp the legacy data-anchor-activity-id that #activity-N deep links rely on.
Nothing mints those links anymore; copy and share links use
#comment-<journal id>, which needs no extra query.
The activity number is now resolved on demand. Only a request carrying
?anchor=activity-N runs the window function, mapping the number to a journal
id the paginator exposes as resolved_anchor. The view hands that to the
client, which rewrites #activity-N to the canonical #comment-<id> and scrolls
using the comment anchor already present in the DOM. Default renders no longer
touch the window function.
References WP #68063.
The feature flag is gone on release/17.5 (PR #23324); the
`with_settings: { work_packages_identifier: ... }` annotation alone
is enough to pin classic vs semantic behaviour in each context.
The earlier write-time canonicalization stored the rendered
display_id ("#PROJ-7") in the journal note, which would rot under
project-identifier renames and leave dangling semantic strings if
semantic mode were rolled back. Restore the PK shape ("#42") and
let the formatter pipeline turn it into the user-facing identifier
at render time, where the resolver already handles both modes.
Both spec contexts now assert the same PK shape; the mode-specific
rendering of "#N" lives in the formatter specs.
The macro preload was visibility-scoped — references to work packages the
recipient cannot see would fall through to the literal `#43` shape, even
when the same reference rendered as `DCP-1` for an author with full view
permission. Notification recipients saw misleading numeric ids for cross-
project references in journal notes.
Splits label resolution from link gating:
- `ResourceLinksMatcher.build_lookup` now does an unscoped fetch for the
primary identifier and a separate visibility-scoped id pluck. The link
handler reads `visible_to_current_user?` to decide between a navigable
anchor and a plain-text label.
- `UpdateAncestorsService#set_journal_note` writes `#display_id` so new
notes carry the semantic shape at the source; render-time resolution
heals legacy `#N` content for users with view permission.
Tradeoff: a recipient without view permission now sees the WP's semantic
identifier (e.g. `DCP-1`) as plain text rather than `#43`. The reference's
existence was already disclosed by the stored journal text; the project
identifier is the only new piece of information surfaced, and is not
treated as a secret elsewhere in the system (URLs, exports, API).
Pagy 43.4.3 deprecated `:max_pages` and recommends capping records
before pagination instead. Caps the combined array in `base_journals`.
https://community.openproject.org/wp/75314
Project#reserve_semantic_id_block! rewrites work_packages.identifier
and sequence_number via a raw SQL UPDATE, leaving the in-memory
records with the nil identifier set by SetAttributesService when the
project changed. Callers that read the WP straight out of the
ServiceResult (HAL action links, the move-and-follow redirect in
WorkPackages::BulkJob#redirect_path) then saw an empty display_id and
fell back to numeric URLs. Reload after the bulk allocation so the
in-memory state matches the database.
find_by(id:) and find_by!(id:) now raise ArgumentError when passed
a semantic identifier string, directing developers to use find() or
the new find_by_display_id() method instead. This avoids silently
altering ActiveRecord's find_by semantics and ensures misuse is
caught in development even when semantic mode is not enabled locally.
Renames find_by_id_or_identifier to find_by_display_id (public API)
and migrates all app callers that receive user-facing strings to use
the new method.
Add find_by and find_by! overrides to FinderMethods so that
find_by(id: "PROJ-42") transparently resolves semantic identifiers,
matching the existing behavior of find and exists?.
Only intercepts calls where id: is the sole keyword — all other
find_by usage (e.g. find_by(subject: ...)) passes through unchanged.
Revert ShowComponent, SplitViewComponent, and WorkPackagesController
from the explicit find_by_id_or_identifier back to standard
find_by(id:) now that the override handles semantic resolution.
Make find_by_id_or_identifier and find_by_id_or_identifier! private
since they are now internal implementation details with no external
callers. Move their test coverage into the find_by/find_by! specs.
The suggestion generator produces uppercase candidates and checks
exclusions with case-sensitive `include?`. Historical slugs stored in
lowercase (e.g. "proj") would not block the generator from suggesting
"PROJ", even though `identifier_not_historically_reserved` would reject
it on save via LOWER(). Upcasing the exclusion set at the consumption
point keeps ProblematicIdentifiers#exclusion_set casing-neutral for
other consumers while ensuring the preview never proposes identifiers
that collide with historical slugs.
Unify test helpers into a single create_project_with_raw_identifier
method that better communicates intent (bypasses validations to set
an exact identifier). Add project.reload after update_all so the
in-memory object stays consistent with the database.
Also documents that the migration rollback is intentionally lossy —
deduplicated identifiers keep their suffixes under the restored
case-sensitive index.
The underscore fix (allowing _ in identifiers per spec) needs to target
ProblematicIdentifiers instead of PreviewQuery after the extraction
refactor on the target branch.
The production spec explicitly allows underscores in alphanumeric
identifiers. PreviewQuery's :special_characters rule and SQL scope
incorrectly flagged them. Updated regex from [^a-zA-Z0-9] to
[^a-zA-Z0-9_] in both the FORMAT_RULES lambda and the
contains_non_alphanumeric scope.
Also added a comment to set_raw_identifier explaining why Arel.sql
is necessary (update_all applies normalizes in this Rails version).
Drop the DB-backed ExclusionSet in favour of a simple Set from pluck.
This is a one-off admin migration — the brief memory cost of loading
all non-problematic identifiers is not worth the added complexity of
a custom duck-typed wrapper.
Also adds performance notes documenting index considerations for the
regex scope conditions and the eager-load trade-off.
Replace the TODO stub in ProblematicIdentifiers#reserved_identifiers
with a real query against the friendly_id_slugs table. Historical slugs
(identifiers a project used in the past but has since changed) are now
excluded from suggestion generation and classified as :reserved in
error_reason output.
The query excludes slugs that match a current active project identifier
(those are already covered by the in_use exclusion path).
Separate scope-building, identifier classification, and exclusion set
logic into a reusable class that both PreviewQuery (admin UI) and the
future ApplyHandlesJob (batch migration) can compose from.
Key changes:
- ProblematicIdentifiers owns FORMAT_RULES, problematic scope, error
classification, and a dual-mode exclusion set (DB-backed ExclusionSet
for preview, preloaded Set for batch)
- PreviewQuery becomes a thin orchestrator delegating to
ProblematicIdentifiers
- Add deterministic ordering (.order(:id)) to preview results
- Rename :not_uppercase → :not_fully_uppercased to match scope method
naming, update corresponding en.yml translation key
- ExclusionSet uses raw SQL to bypass Rails normalizes on :identifier
which would transform query parameters based on current setting mode
Expand PreviewQuery to detect additional alphanumeric identifier
format violations: case-mismatch, underscores, leading digits, and
purely numerical identifiers. Restructure error classification with
FORMAT_RULES for clarity and add corresponding locale strings.
Update MCP tool descriptions for search_projects, search_programs,
and search_portfolios to reflect case-insensitive identifier
matching, with updated test expectations.
Replace the separate `reserved_identifiers` and `in_use_identifiers` keyword arguments with a single `exclude` parameter.
The generator immediately merged these two sets on every call path -- the distinction only matters to `PreviewQuery` for error classification, which it already handles independently. Collapsing them into one parameter removes unnecessary coupling.
- Flatten multi-word candidate generation from 5 methods to 4 by
removing unnecessary indirection layers
- Apply ensure_starts_with_letter in numeric_suffix_fallback so
fallback identifiers also satisfy the starts-with-letter constraint
- Add test verifying batch mode assigns identifiers in array order
- Replace numeric suffix collision strategy with progressive acronym
widening ("SC" → "STC" → "STCO" instead of "SC" → "SC2" → "SC3")
- Allow underscores in identifiers (fix regex in PreviewQuery)
- Enforce identifiers must start with a letter (strip leading digits)
- Use DEFAULT_IDENTIFIER_BASE_LENGTH (5) for initial generation with
MAX_IDENTIFIER_LENGTH (10) as expansion ceiling for collisions
- Enforce MIN_IDENTIFIER_LENGTH (2) for generated identifiers
- Expand single-word identifiers on collision ("BAN" → "BANA" → "BANAN")
Aligns the class name and all internal terminology with the domain
language: "handle" → "identifier" throughout. Renames the file, class,
constants (HANDLE_MAX_LENGTH → IDENTIFIER_MAX_LENGTH, FALLBACK_HANDLE →
FALLBACK_IDENTIFIER), public API (suggest_handle → suggest_identifier,
suggested_handle hash key → suggested_identifier), keyword arguments
(in_use_handles → in_use_identifiers, reserved_handles →
reserved_identifiers), and private helper methods accordingly. All
call-sites and specs updated to match.
- Move DISPLAY_COUNT constant from IdentifierAutofixSectionComponent to
PreviewQuery, eliminating a service-layer dependency on a view component.
The component now forwards to PreviewQuery::DISPLAY_COUNT.
- Guard PreviewQuery.new.call to only run in the :edit state.
Previously it executed on every render, hitting the DB twice per
Hotwire status-poll during the :change_in_progress phase.
- Replace nil guard in error_label with I18n.t default: "" to cover
any unrecognised error reason, not just nil.
- Add component spec for IdentifierSettingsFormComponent covering all
three states (:change_in_progress, :completed, :edit) including the
autofix-section visibility branch.
- Update preview_query_spec to reference PreviewQuery::DISPLAY_COUNT
directly instead of the UI component constant.
Single-word names previously produced a 1-char handle ("Banana" → "B").
Add a SINGLE_WORD_LENGTH = 3 constant and branch handle_from_name so
single-word names return the first 3 transliterated, uppercased chars
("Banana" → "BAN", "Kiwi" → "KIW"). Multi-word names continue to use
the initials/acronym path unchanged.
Documents and pins the behaviour for scripts without transliteration
entries (Japanese, Chinese, Arabic, …):
- Fully non-Latin name (e.g. "日本語プロジェクト"): every initial
maps to "?" via I18n.transliterate, filter_map drops them all,
empty acronym falls back to FALLBACK_HANDLE ("PROJ").
- Mixed name (e.g. "Plan 日本"): Latin initials survive, non-Latin
ones are silently dropped, result is the Latin-only acronym ("P").
- handle_from_name: replace /[a-zA-Z0-9]+/ with /[[:alpha:][:digit:]]+/
so accented letters (é, ñ, ü…) are kept inside their word rather than
treated as separators. "Cécile Martin" now produces "CM" instead of "CCM".
Transliterate each word's first character via I18n.transliterate (consistent
with app/models/exports/exporter.rb) before uppercasing, so non-ASCII
initials map to their ASCII equivalent (é→E, ñ→N). filter_map silently
drops any initial that produces no usable character after transliteration.
- Service spec: remove the receive_message_chain stub entirely. All fixtures
are now real create(:project, ...) records so the SQL query is exercised
against the test DB. shared_let used where the same record backs multiple
it-blocks; inline create for single-assertion tests. Two new Unicode examples:
"Cécile Martin" → "CM" and "étude de cas" → "EDC".
- ProjectHandleSuggestionGenerator: replace Project.all.to_a + Ruby-side
filter with a SQL-filtered, column-minimal query (select :id/:name/:identifier,
WHERE length > 10 OR non-alphanumeric). Adds FIXME(project_handles) markers
showing the exact ProjectHandle query to swap in once the data model exists.
Adds inline docs explaining the unique_handle collision-resolution algorithm.
- IdentifierSettingsFormComponent template: restructure for full-width banner
and table. The form (radio buttons only) stays inside the 680px
settings_primer_form_with wrapper and gets id="wp-identifier-settings-form".
The autofix section (banner + table) is a sibling div outside the wrapper.
The submit button lives in its own 680px wrapper and links back to the form
via the HTML5 form="wp-identifier-settings-form" attribute.