456 Commits

Author SHA1 Message Date
Kabiru Mwenja f94d7b0bfc Resolve the changes-filter predecessor once in a journals CTE
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.
2026-06-09 10:25:21 +03:00
Kabiru Mwenja fd22629702 Code Maintenance/STC-462: Tidy the activity-tab paginator and centralise filter modes (#23552)
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.
2026-06-05 16:50:08 +03:00
Kabiru Mwenja 5fdf57df69 Defer activity-tab sequence_version to anchor resolution
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.
2026-06-05 14:26:04 +03:00
Kabiru Mwenja 376f5fcdc3 Code Maintenance/STC-462: Move work package activity tab from in-memory to database-level pagination (#23434) 2026-06-05 14:21:47 +03:00
Oliver Günther 11dc79a74d Merge remote-tracking branch 'origin/release/17.5' into dev 2026-06-02 16:22:39 +02:00
Oliver Günther 9993792dbf Merge remote-tracking branch 'origin/release/17.4' into release/17.5 2026-06-02 16:22:18 +02:00
Oliver Günther 2a6412d5ae Merge remote-tracking branch 'origin/release/17.3' into release/17.4 2026-06-02 16:22:05 +02:00
Oliver Günther bef69b6aaf Parse query params as a separate options hash, not kwargs 2026-06-02 16:21:14 +02:00
OpenProject Actions CI 49b802ca02 Merge branch 'release/17.5' into dev 2026-05-28 08:51:09 +00:00
Kabiru Mwenja 23d52fcf1c Drop semantic_work_package_ids flag annotations
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.
2026-05-26 14:24:03 +03:00
Kabiru Mwenja 6e3068531f Write primary-key WP refs in auto-generated journal notes
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.
2026-05-26 14:19:25 +03:00
Kabiru Mwenja 878048f8e8 Resolve WP labels across visibility boundaries in text macros
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).
2026-05-26 14:18:48 +03:00
Alexander Brandon Coles 1ba2dd2775 [#75314] Replace deprecated Pagy max_pages option
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
2026-05-26 11:56:20 +02:00
Tomas Hykel 948fa43321 chore: Remove feature flag for project-based work package identifiers 2026-05-25 17:45:06 +02:00
Kabiru Mwenja a54362020f Refresh in-memory identifier after WP move re-allocation
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.
2026-04-30 16:56:27 +03:00
Tomas Hykel 0f9ea744c6 [#73614] Improve validation of semantic identifiers for WPs 2026-04-22 16:34:07 +02:00
Tomas Hykel 150d2ba2a3 add an integration test for the scenario 2026-04-22 09:04:17 +02:00
Tomas Hykel c85a69c476 [#74192] Clear semantic identifier when moving a work package 2026-04-21 22:35:47 +02:00
Tomas Hykel 9b38c082ab [#73613] Improve peformance of bulk semantic ID allocation 2026-04-21 22:26:25 +02:00
Kabiru Mwenja 79d4e67a0d Merge pull request #22718 from opf/feature/73756-adapt-routes-for-project-based-semantic-work-package-identifiers
Make find/exists? resolve semantic work package identifiers
2026-04-21 13:13:54 +03:00
Kabiru Mwenja 7824f5908c Handle string-keyed hashes in find_by semantic ID guard
AR sometimes passes string keys ("id") instead of symbol keys (:id)
to find_by. Check both forms when detecting semantic identifiers.
2026-04-17 19:05:36 +03:00
Kabiru Mwenja 462a0f1c78 Replace transparent find_by resolution with ArgumentError guard
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.
2026-04-17 18:17:15 +03:00
Tomas Hykel 3a7fad89df [#73711] refactor: Rename the project identifier namespace 2026-04-16 21:40:38 +02:00
Kabiru Mwenja a898486c3a Override find_by/find_by! for semantic ID resolution
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.
2026-04-15 15:54:48 +03:00
Tomas Hykel c4debc8aaa [#73523] Implement WorkPackage semantic ID allocation system 2026-04-01 17:25:19 +02:00
Kabiru Mwenja 4812974de9 Upcase exclusion set for case-insensitive slug matching
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.
2026-03-25 12:16:10 +03:00
Kabiru Mwenja f0ff8030ec Cleanup tests and add lossy rollback note
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.
2026-03-24 08:10:18 +03:00
Kabiru Mwenja 2bf5740f6a Cleanup tests 2026-03-23 18:21:41 +03:00
Kabiru Mwenja 5c7bed193e Apply underscore fix to ProblematicIdentifiers after rebase
The underscore fix (allowing _ in identifiers per spec) needs to target
ProblematicIdentifiers instead of PreviewQuery after the extraction
refactor on the target branch.
2026-03-23 17:24:55 +03:00
Kabiru Mwenja ac50f72443 Allow underscores in PreviewQuery identifier classification
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).
2026-03-23 17:22:40 +03:00
Kabiru Mwenja 6c8421345a Simplify exclusion_set to plain Set, remove ExclusionSet class
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.
2026-03-20 23:06:26 +03:00
Kabiru Mwenja 30f9c7c633 Wire up reserved identifiers with FriendlyId slug history
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).
2026-03-20 22:57:07 +03:00
Kabiru Mwenja 69818ad10c Extract ProblematicIdentifiers from PreviewQuery
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
2026-03-20 22:55:54 +03:00
Kabiru Mwenja 91362fb680 Improve PreviewQuery error detection and update MCP descriptions
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.
2026-03-20 21:35:08 +03:00
Kabiru Mwenja 480db19b19 Simplify ProjectIdentifierSuggestionGenerator interface (#22439)
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.
2026-03-20 20:25:37 +03:00
Kabiru Mwenja 68ef621217 Simplify multi-word pipeline, harden digit-stripping, add ordering test
- 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
2026-03-13 18:24:14 +03:00
Kabiru Mwenja 0053b140a9 Consolidate identifier length constants into IDENTIFIER_LENGTH hash
Group the four related length constants (min, max, base, single_word)
into a single frozen hash for better locality and fewer top-level names.
2026-03-13 18:06:14 +03:00
Kabiru Mwenja f2b23e0d15 Fix identifier generator to match semantic identifier spec
- 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")
2026-03-13 18:01:11 +03:00
Kabiru Mwenja e09288dd6a Address code review: rename column, remove padding, split constants, extract error_reason 2026-03-13 17:07:03 +03:00
Kabiru Mwenja e9900dfe2d Rename ProjectHandleSuggestionGenerator to ProjectIdentifierSuggestionGenerator
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.
2026-03-13 16:38:00 +03:00
Kabiru Mwenja aaaaaef731 Add suggest_handle static interface to ProjectHandleSuggestionGenerator 2026-03-13 16:30:36 +03:00
Kabiru Mwenja 36514d9571 Address code review concerns for identifier settings UI
- 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.
2026-03-07 14:17:29 +03:00
Kabiru Mwenja ebc0e4dd79 Fix handle_from_name for single-word project names
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.
2026-03-07 13:31:59 +03:00
Kabiru Mwenja 1ef8ee6380 Touch up project handle suggestion generator specs 2026-03-05 12:38:30 +03:00
Kabiru Mwenja 4701835187 Relocate ProjectHandleSuggestionGenerator into WorkPackages::IdentifierAutofix 2026-03-05 12:27:45 +03:00
Kabiru Mwenja f1655e953c Extract PreviewQuery, collapse error_label, remove unused ERROR_REASONS 2026-03-05 12:24:35 +03:00
Kabiru Mwenja 180a17a99a Extend handle generator with in_use/reserved reasons and performance-aware preview 2026-03-05 11:53:25 +03:00
Kabiru Mwenja def97b72fd Test non-Latin script fallback in handle generation
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").
2026-02-26 12:09:38 +03:00
Kabiru Mwenja 2576d57dd9 Improve handle suggestion algo: Unicode support + real-DB spec
- 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".
2026-02-25 22:28:10 +03:00
Kabiru Mwenja 8ca8e9d8d1 Refactor: SQL query, full-width layout, docs, FIXME markers
- 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.
2026-02-25 22:27:52 +03:00