Skip to content

feat: Community translation v1.1 — Contributors (pitch #3806)#3819

Open
Anty0 wants to merge 6 commits into
mainfrom
jirikuchynka/community-contributors
Open

feat: Community translation v1.1 — Contributors (pitch #3806)#3819
Anty0 wants to merge 6 commits into
mainfrom
jirikuchynka/community-contributors

Conversation

@Anty0

@Anty0 Anty0 commented Jul 23, 2026

Copy link
Copy Markdown
Member

Integration branch for pitch #3806 — Community translation v1.1 (Contributors).

Pitch: #3806

All parts of the pitch land on this branch: each remaining part opens its own PR
targeting jirikuchynka/community-contributors (not main), and this PR merges to
main once the pitch is complete. Scope A (below) is the first part landed here.


Scope A — Contributor tracking + list endpoint (backend) ✅

A contributor = someone who did any modifying action (per the activity log) on a
project and is not currently a member of it.

  • project_contributor cache table, maintained by a Postgres AFTER INSERT trigger
    on activity_revision — a single-row upsert per contribution (LEAST/GREATEST on
    the timestamp), atomic with the activity write. Like activity_revision it carries
    no foreign keys, so it mirrors the append-only log; its rows outlive a deleted
    project/user and the read query filters them out.
  • One-time backfill migration over existing activity history (mirrors the trigger's
    rule; excludes historical null-typed no-op revisions).
  • GET /v2/projects/{projectId}/contributors — paged, MEMBERS_VIEW + super-auth,
    @AllowApiAccess, email-free (name + avatar only). Excludes current members
    (direct permission or org role), deleted and disabled users. Sort allowlist
    (lastContributionAt / firstContributionAt) with a user_id tiebreaker.
  • Activity-store fix: null-typed revisions with no modifications are no longer
    persisted, so revision exists ⇒ real contribution.
  • Index on project_contributor(user_id) for the upcoming per-user query (scope C);
    the per-project read is served by the (project_id, user_id) primary key.

Design and changeset were converged through the full multi-reviewer thorough-review loop.

Scope C — /community-projects page: toggle + switcher entry (#3820) ✅

Reads the project_contributor table from the per-user direction.

  • "My contributions only" toggle, default on — lists public projects the user
    contributed to but is not a member of; off lists all public projects. Sits in the
    header's add-button slot (where the normal list's add button lives). Served by an
    opt-in filterContributed param on GET /v2/public/projects/with-stats, so the
    anonymous public-homepage list that shares the hook is unchanged.
  • Re-wired "Community translation" org-switcher entry, gated on a new
    InitialDataModel.hasCommunityContributions flag — a cheap EXISTS over the
    project_contributor(user_id) index, piggybacked on the cached initial-data payload
    (no extra request). Appears exactly when the community page has something to show.
  • The list filter and the switcher gate share a single NON_MEMBER_CONTRIBUTOR_FILTER
    constant, so they cannot diverge.
  • Re-enabled the five skipped communityProjectsNavigation.cy.ts tests, plus new
    default-on and negative-branch (no-contributions → no entry) coverage.

Converged through the full multi-reviewer thorough-review loop.

Scope B — Contributors on the Members page (#3821) ✅

Reads the scope-A contributors endpoint and surfaces it on the Members page.

  • Team / Community tabs, shown only when the project is public and has ≥1
    contributor; the tab selection lives in the URL (?tab=) like the page's
    agency filter. When the gate is not met the member list renders unchanged.
  • Community row: avatar, name (with an unnamed fallback), and first/last
    contribution dates — email-free by construction (no email in the model,
    row, or payload; the member assembler's masking is not reused).
  • Read-only; the optional "Invite as member" button (scope D) is not included.
  • e2e: a Cypress spec covering tab gating (public-with-contributors /
    public-empty / private), Team↔Community mutual exclusivity, and the no-email
    assertion, driven off the shared ContributorsTestData fixture via a new e2e
    data controller.
  • Also fixed a latent teardown bug: cleanTestData looked users up via
    findActive, so deleted/disabled fixture users leaked and collided on the
    next generate; it now tears them down regardless of state.

Converged through the full multi-reviewer thorough-review loop.

Scope D — Invite a contributor as a member (#3825) ✅

The optional invite path from the Community row: turn a listed contributor into a
pending member without ever exposing their email.

  • PUT /v2/projects/{projectId}/invite-contributor — the request carries a
    user id, not an address. The server resolves the target through the same
    VISIBLE_CONTRIBUTOR filter the list uses (findVisibleContributor), so a caller can
    only invite someone the contributors list would already show them. A non-visible id
    (member / deleted / disabled / non-contributor) returns the same uniform 404 as a
    missing project — the endpoint is not a membership/existence oracle.
  • Email stays hidden. Invitations store the address as before, but a new
    Invitation.emailHidden flag gates a visibleEmail getter that every invitation
    assembler (project / public / organization) now reads — so a pending user-targeted
    invitation never republishes the address onto the Members page. The reveal is gated,
    not the storage: validateInvitationEmail early-returns on a blank email, so
    dropping storage would turn these into shareable invite codes.
  • Scopes-vs-role routing lives in the controller (EE dispatch); a NONE-permission
    invite is rejected (CANNOT_INVITE_CONTRIBUTOR_WITH_NO_PERMISSION).
  • "Invite as member" button on the Community row, gated on members.edit; opens the
    standard permissions modal with the NONE role hidden.
  • e2e: a Cypress spec covering the gated button, the permissions flow, and the
    invite request shape.

Converged through the full multi-reviewer thorough-review loop.

Hardening — stop disclosing user e-mail (username) platform-wide (#3823) ✅

Not a planned pitch scope — surfaced by the pitch's own risk that maskedMemberField
does not protect us. A user's username is their e-mail, and the old masking gated
only on the caller holding MEMBERS_VIEW, never on the target being a member, and
failed open off-request/off-project. So a project admin on a public project saw the raw
e-mail of community contributors and ex-members across the activity feed, suggestions,
comments, history, tasks, and key-trash; non-project-scoped endpoints (notifications,
co-assignees) leaked to anyone.

  • username stripped to a deprecated always-empty field on every model that merely
    references a user (SimpleUserAccountModel — which backs ~16 assemblers — plus the
    activity-author and api-key models). Kept on the wire so existing clients don't break.
  • Disclosure now holds by construction, not by re-checking membership on ~11 surfaces.
    The e-mail survives only where it is legitimate: project-members list, org-members list,
    self/me, instance-admin user management, and the admin-only export ZIP (import re-links
    authors by username).
  • UsernameDisclosureGuard — a classpath-scanning test (core + EE entrypoints) fails
    CI if any non-sanctioned response model reintroduces a username, so the invariant cannot
    silently regress as new author-bearing surfaces are added.
  • Also closed a raw deleter-e-mail leak on the key-search endpoint and dropped several dead
    author-email view fields the review surfaced.

Converged through the full multi-reviewer thorough-review loop.

Remaining parts

  • Docs — community-translation docs mentioning contributors (optional).

Design notes (rationale moved out of code comments)

Backfill runs inline at boot — why that is acceptable. The one-time historical
backfill (projectContributorBackfill.sql) aggregates activity_revision, a
high-row-count table, in a single Liquibase transaction at startup. Kept on the boot
path deliberately:

  • Cost ceiling. activity_revision is high-row-count but narrow — the large jsonb
    payloads live in child tables (activity_modified_entity / activity_describing_entity),
    not here — so this is a single seq-scan + hash-aggregate: seconds to low-minutes even on
    the largest known instance (~1e7 rows). The EXISTS is served by the
    activity_modified_entity FK index.
  • It takes an MVCC snapshot and does not lock activity_revision writers.
  • It is idempotent (ON CONFLICT LEAST/GREATEST), so a re-run after a raised
    statement_timeout is safe.
  • It is non-urgent: the track_project_contributor trigger is installed by the
    preceding changeset, so all go-forward writes are already captured — the backfill is
    pure historical catch-up and could be run out of band if a deployment ever needed to
    skip it. The residual all-or-nothing boot-block risk on a pathologically large instance
    is accepted; operators can raise statement_timeout if ever needed.

Staff are counted as contributors. The membership exclusion filters on membership
rows only (a Permission or OrganizationRole on the project). A global ADMIN/SUPPORTER
who edits a project without holding one is intentionally listed — the pitch definition is
"acted and is not a member", with no staff carve-out. Excluding them would be a one-line
u.role filter on findContributors / NON_MEMBER_CONTRIBUTOR_FILTER if ever desired.

project_contributor lifecycle. The table is FK-free (mirroring activity_revision).
Its rows are reclaimed by ProjectHardDeletingService when a project is hard-deleted;
users are only ever soft-deleted, so their rows persist and are filtered at read.

Hot-path flag query shape. hasCommunityContributions (on every authenticated
initial-data load) is hasNonMemberPublicContribution, written to drive from the
project_contributor(user_id) index — it touches only the user's few contribution rows
rather than scanning every public project.

Summary by CodeRabbit

  • New Features
    • Added a Community tab to public project members pages, showing contributor names, avatars, and contribution dates.
    • Project managers can invite visible contributors as project members with configurable permissions.
    • Added a “My contributions only” filter for public community projects.
    • Initial data now indicates whether users have community contributions.
  • Privacy
    • Contributor and invitation responses hide email addresses.
    • Usernames are no longer exposed in non-member references.
  • Bug Fixes
    • Improved contribution tracking, filtering, pagination, and activity-date accuracy.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds database-backed project contributor tracking, a secured paginated contributor API, public-project contribution filtering, contribution state in initial data, community navigation controls, contributor invitations, Team/Community member tabs, and username disclosure changes.

Changes

Project contributors and community contributions

Layer / File(s) Summary
Activity tracking and persistence
backend/data/..., backend/development/..., backend/app/src/test/...
Adds trigger-maintained contributor rows, historical backfill, timestamp merging, cleanup handling, export/import exclusion, and null-typed activity storage rules.
Contributor query and secured API
backend/data/..., backend/api/...
Queries active non-member contributors, applies deterministic sorting, and exposes a paginated HATEOAS endpoint with permission checks.
Public-project contribution filtering
backend/data/..., backend/api/...
Adds filterContributed and reports authenticated users’ non-member public contribution state.
Contributor invitations
backend/api/..., backend/data/..., webapp/...
Adds visible-contributor invitations, permission validation, hidden invitation emails, and contributor invitation UI flows.
Community navigation and member tabs
webapp/src/views/projects/..., webapp/src/component/organizationSwitch/*
Gates community navigation, adds a “My contributions only” filter, and renders Team/Community project-member tabs.
Username disclosure and permission helpers
backend/api/..., backend/data/..., webapp/..., ee/...
Deprecates or strips usernames from non-allowlisted response models, centralizes permission payload construction, and updates related projections and tests.
Fixtures and validation
backend/app/src/test/..., e2e/cypress/..., ee/backend/tests/...
Adds activity fixtures and coverage for contributor filtering, invitations, navigation, tabs, hidden emails, and username disclosure.

Estimated code review effort: 5 (Critical) | ~100 minutes

Possibly related issues

Suggested labels: enhancement

Suggested reviewers: jancizmar

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly relates to the main change set around Community translation contributors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jirikuchynka/community-contributors

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/NullTypedActivityRevisionStorageTest.kt`:
- Around line 28-30: Make contributor-cache cleanup project-scoped: in
NullTypedActivityRevisionStorageTest.cleanup, explicitly delete
project_contributor rows for this test project or update the cleanup service
accordingly; in ProjectContributorBackfillTest, replace the global delete with a
project_id-filtered delete using that test’s project. Do not retain any unscoped
contributor-row deletion.

In `@backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt`:
- Around line 203-204: Update the ActivityService persistence skip logic around
saveWithoutModification so a null-typed revision is not skipped when
activityRevision.describingRelations is non-empty. Keep relation persistence
through persistDescribingRelations independent from modified-entity persistence,
and add a regression test covering a relation-only null-typed revision.

In
`@backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ContributorsTestData.kt`:
- Around line 10-20: Update ContributorsTestData to extend BaseTestData and
remove its manual TestDataBuilder ownership, while preserving the existing
fixture fields and setup behavior through the inherited base-test-data contract.

In `@backend/data/src/main/resources/db/changelog/schema.xml`:
- Around line 5835-5840: The rollback in changeSet 1784467200000-3 is
destructive because it deletes all project_contributor rows. Replace the DELETE
rollback with a no-op or a reversible backfill-specific restoration that
preserves trigger-created and pre-existing cache data; keep the forward
projectContributorBackfill.sql operation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 76b9713a-fa6b-4d5b-90b8-a7def5c9ba1d

📥 Commits

Reviewing files that changed from the base of the PR and between 7c9823d and 856408d.

⛔ Files ignored due to path filters (2)
  • webapp/src/service/apiSchema.generated.ts is excluded by !**/*.generated.*
  • webapp/src/service/apiSchemaTypes.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (17)
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectContributorsController.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModelAssembler.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/NullTypedActivityRevisionStorageTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorBackfillTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorsControllerTest.kt
  • backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ContributorsTestData.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributor.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributorId.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/ProjectContributorView.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectContributorRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/service/contributor/ProjectContributorService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/projectExportImport/ProjectExportImportPolicyRegistry.kt
  • backend/data/src/main/resources/db/changelog/projectContributorBackfill.sql
  • backend/data/src/main/resources/db/changelog/projectContributorTrigger.sql
  • backend/data/src/main/resources/db/changelog/schema.xml

Comment thread backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt
Comment thread backend/data/src/main/resources/db/changelog/schema.xml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts (1)

20-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Place the suite before this helper.

This low-level helper appears before the top-level describe blocks. Move it below the suites or into shared test helpers so the file follows the Stepdown Rule.

As per path instructions, “The most important, high-level concepts should be at the top” and callers should appear before functions they call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts` around lines 20 -
23, Move the low-level disableMyContributions helper below the top-level
describe suites in the test file, or relocate it to shared test helpers, so the
suite declarations and other high-level concepts appear before the helper they
call.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PublicProjectsControllerTest.kt`:
- Around line 255-269: Move the activity fixture setup out of recordActivity and
into the relevant TestData class. Create ActivityRevision instances through that
TestData graph, persist them with testDataService, and clean them in the suite’s
`@AfterEach` using the existing TestData lifecycle pattern; remove the direct
entityManager.persist/flush setup from recordActivity while preserving its
projectId, authorId, and timestamp behavior.

In
`@backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/CommunityContributionE2eDataController.kt`:
- Around line 31-35: Replace the empty ActivityRevision persisted by the
controller with a typed revision containing a valid modification for
publicProject in
backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/CommunityContributionE2eDataController.kt
lines 31-35, and apply the same valid activity shape for outsiderProject in
backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/PublicProjectsE2eDataController.kt
lines 33-37. Ensure both persisted revisions define a type and modifications,
avoiding null-typed records.

In `@e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts`:
- Around line 300-301: Replace the cy.contains() assertions in the community
project navigation test with gcy()/cy.gcy() selectors. Add a stable static
data-cy value to each project row and expose the project identity through a
separate data-cy-item attribute, then assert visibility and nonexistence using
those attributes instead of rendered text.

---

Nitpick comments:
In `@e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts`:
- Around line 20-23: Move the low-level disableMyContributions helper below the
top-level describe suites in the test file, or relocate it to shared test
helpers, so the suite declarations and other high-level concepts appear before
the helper they call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e4ecdb91-7a6b-42cb-ad80-cbcfddb03b89

📥 Commits

Reviewing files that changed from the base of the PR and between 856408d and 4caf8fd.

⛔ Files ignored due to path filters (1)
  • webapp/src/service/apiSchema.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (21)
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/InitialDataController.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/PublicProjectsController.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/initialData/InitialDataModel.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/InitialDataControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PublicProjectsControllerTest.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/CommunityContributionE2eData.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/PublicProjectsE2eData.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectContributorRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/service/contributor/ProjectContributorService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/project/ProjectService.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/CommunityContributionE2eDataController.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/PublicProjectsE2eDataController.kt
  • e2e/cypress/common/apiCalls/testData/testData.ts
  • e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts
  • e2e/cypress/support/dataCyType.d.ts
  • webapp/src/component/organizationSwitch/OrganizationSwitch.tsx
  • webapp/src/globalContext/helpers.tsx
  • webapp/src/views/projects/CommunityProjectsView.tsx
  • webapp/src/views/projects/MyContributionsToggle.tsx
  • webapp/src/views/projects/usePublicProjectsList.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/data/src/main/kotlin/io/tolgee/service/contributor/ProjectContributorService.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectContributorRepository.kt

Comment thread e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
webapp/src/views/projects/members/ProjectMembersView.tsx (1)

55-97: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider gating team vs. community queries by the active tab.

membersLoadable and invitationsLoadable have no enabled condition, so they keep fetching even while showCommunity is true and the Team section is only CSS-hidden. Gating them (and vice versa for contributorsLoadable, which already has enabled: project.public) avoids redundant network calls whenever a user is on the Community tab.

♻️ Proposed refactor
   const membersLoadable = useApiQuery({
     url: '/v2/projects/{projectId}/users',
     method: 'get',
     path: { projectId: project.id },
     query: {
       page,
       sort: ['name'],
       search,
       filterAgency: filterAgency?.map((a) => Number(a)),
     },
     options: {
+      enabled: !showCommunity,
       keepPreviousData: true,
     },
   });

   const invitationsLoadable = useApiQuery({
     url: '/v2/projects/{projectId}/invitations',
     method: 'get',
     path: { projectId: project.id },
     options: {
+      enabled: !showCommunity,
       keepPreviousData: true,
     },
   });

Note: showCommunity is computed after these two loadables, so this would need reordering or a showCommunity-independent flag (e.g. derive from activeTab directly) to avoid a forward reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/views/projects/members/ProjectMembersView.tsx` around lines 55 -
97, Gate the team queries in membersLoadable and invitationsLoadable so they are
enabled only when the Team tab is active, while retaining any required project
conditions. Update contributorsLoadable to enable only for public projects on
the Community tab, using an activeTab-derived condition or reordering the
showCommunity computation to avoid a forward reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/cypress/e2e/projects/communityMembersTab.cy.ts`:
- Around line 53-54: Update the contributor absence assertions in the community
members test to scope them through the contributor list’s data-cy selector and
assert that it does not contain either email. Use the project’s typed
gcy()/cy.gcy() helper and avoid raw cy.contains() text searches.

---

Nitpick comments:
In `@webapp/src/views/projects/members/ProjectMembersView.tsx`:
- Around line 55-97: Gate the team queries in membersLoadable and
invitationsLoadable so they are enabled only when the Team tab is active, while
retaining any required project conditions. Update contributorsLoadable to enable
only for public projects on the Community tab, using an activeTab-derived
condition or reordering the showCommunity computation to avoid a forward
reference.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0c6ef4ec-3636-4c92-a5f1-0449f61b568b

📥 Commits

Reviewing files that changed from the base of the PR and between 4caf8fd and 34b9f46.

📒 Files selected for processing (9)
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/TestDataService.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ContributorsTestData.kt
  • backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/MembersCommunityE2eDataController.kt
  • e2e/cypress/common/apiCalls/testData/testData.ts
  • e2e/cypress/e2e/projects/communityMembersTab.cy.ts
  • e2e/cypress/support/dataCyType.d.ts
  • webapp/src/views/projects/members/ProjectMembersView.tsx
  • webapp/src/views/projects/members/component/ContributorItem.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/cypress/common/apiCalls/testData/testData.ts

Comment thread e2e/cypress/e2e/projects/communityMembersTab.cy.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (1)
backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/AbstractE2eDataController.kt (1)

54-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Restore top-down method ordering.

Move generate above afterTestDataStored and recordContributorActivity; its callback currently appears before its caller. As per path instructions, “Functions should be ordered so that a caller appears before the functions it calls.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/AbstractE2eDataController.kt`
around lines 54 - 76, Restore top-down ordering in AbstractE2eDataController by
moving the generate method above afterTestDataStored and
recordContributorActivity. Keep the method implementations unchanged; only
reorder them so callers appear before the functions they invoke.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorBackfillTest.kt`:
- Line 44: Replace each table-wide cleanup in the three backfill tests with a
DELETE scoped to the fixture project, using the project identifier established
by each test. Update all occurrences in the test methods around the existing
backfill invocation, preserving cleanup of only that test’s project
contributors.

In
`@backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/MembersCommunityE2eDataController.kt`:
- Around line 10-20: Remove the singleton-scoped currentTestData dependency in
MembersCommunityE2eDataController by passing the ContributorsTestData generated
in testData through the afterTestDataStored callback, or otherwise storing it in
request-local state. Ensure each callback uses the data from its own request
when recording contributor activity, without sharing mutable fixture state
across requests.

---

Nitpick comments:
In
`@backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/AbstractE2eDataController.kt`:
- Around line 54-76: Restore top-down ordering in AbstractE2eDataController by
moving the generate method above afterTestDataStored and
recordContributorActivity. Keep the method implementations unchanged; only
reorder them so callers appear before the functions they invoke.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51a38430-47f7-47b2-973a-025f7f434e89

📥 Commits

Reviewing files that changed from the base of the PR and between 34b9f46 and 70a1fe0.

⛔ Files ignored due to path filters (2)
  • webapp/src/service/apiSchema.generated.ts is excluded by !**/*.generated.*
  • webapp/src/service/apiSchemaTypes.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (48)
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/InitialDataController.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectContributorsController.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/PublicProjectsController.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/initialData/InitialDataModel.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/InitialDataControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/NullTypedActivityRevisionStorageTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorBackfillTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorsControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PublicProjectsControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/contributors/ContributorActivityRecorder.kt
  • backend/app/src/test/kotlin/io/tolgee/service/UserAccountServiceTest.kt
  • backend/app/src/test/kotlin/io/tolgee/service/project/ProjectHardDeletingServiceTest.kt
  • backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/TestDataService.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/CommunityContributionE2eData.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ContributorsTestData.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/PublicProjectsE2eData.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributor.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributorId.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/ProjectContributorView.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectContributorRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/service/contributor/ProjectContributorService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/project/ProjectHardDeletingService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/project/ProjectService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/projectExportImport/ProjectExportImportPolicyRegistry.kt
  • backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt
  • backend/data/src/main/resources/db/changelog/projectContributorBackfill.sql
  • backend/data/src/main/resources/db/changelog/projectContributorTrigger.sql
  • backend/data/src/main/resources/db/changelog/schema.xml
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/AbstractE2eDataController.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/CommunityContributionE2eDataController.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/MembersCommunityE2eDataController.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/PublicProjectsE2eDataController.kt
  • e2e/cypress/common/apiCalls/testData/testData.ts
  • e2e/cypress/e2e/projects/communityMembersTab.cy.ts
  • e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts
  • e2e/cypress/support/dataCyType.d.ts
  • webapp/src/component/organizationSwitch/OrganizationSwitch.tsx
  • webapp/src/globalContext/helpers.tsx
  • webapp/src/views/projects/CommunityProjectsView.tsx
  • webapp/src/views/projects/MyContributionsToggle.tsx
  • webapp/src/views/projects/members/ProjectMembersView.tsx
  • webapp/src/views/projects/members/component/ContributorItem.tsx
  • webapp/src/views/projects/usePublicProjectsList.ts
🚧 Files skipped from review as they are similar to previous changes (29)
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModel.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributor.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/TestDataService.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModelAssembler.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/ProjectContributorView.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/initialData/InitialDataModel.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributorId.kt
  • e2e/cypress/support/dataCyType.d.ts
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/CommunityContributionE2eData.kt
  • backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/projectExportImport/ProjectExportImportPolicyRegistry.kt
  • backend/data/src/main/resources/db/changelog/projectContributorBackfill.sql
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/InitialDataControllerTest.kt
  • webapp/src/views/projects/usePublicProjectsList.ts
  • e2e/cypress/common/apiCalls/testData/testData.ts
  • webapp/src/views/projects/CommunityProjectsView.tsx
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/CommunityContributionE2eDataController.kt
  • backend/data/src/main/resources/db/changelog/projectContributorTrigger.sql
  • backend/data/src/main/kotlin/io/tolgee/service/contributor/ProjectContributorService.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/NullTypedActivityRevisionStorageTest.kt
  • webapp/src/views/projects/members/component/ContributorItem.tsx
  • backend/data/src/main/kotlin/io/tolgee/service/project/ProjectService.kt
  • webapp/src/component/organizationSwitch/OrganizationSwitch.tsx
  • webapp/src/views/projects/members/ProjectMembersView.tsx
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PublicProjectsControllerTest.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/InitialDataController.kt
  • backend/data/src/main/resources/db/changelog/schema.xml
  • e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts
  • e2e/cypress/e2e/projects/communityMembersTab.cy.ts

@Anty0
Anty0 force-pushed the jirikuchynka/community-contributors branch from 70a1fe0 to 37ab36d Compare July 25, 2026 15:58
Anty0 added a commit that referenced this pull request Jul 25, 2026
Part of pitch #3806 (Community translation v1.1 — Contributors), **scope
D**. Targets the integration branch (PR #3819), not `main`.

## What

Adds an **"Invite as member"** button to a Community-tab row on the
project Members page. Clicking it invites that contributor to the
project **by user id** — the server resolves their address, sends the
invitation mail, and never returns it to the org.

- New endpoint `PUT /v2/projects/{projectId}/invite-contributor`
(`MEMBERS_EDIT`); the body carries `userId` + permission fields, **no
email**.
- **Email-free by construction.** The address is stored server-side
(needed to send the mail and bind acceptance) but is masked at read time
by `Invitation.visibleEmail` — the single home of the rule — which
**every** assembler that serializes an invitee address reads (Team-tab
pending list *and* the unauthenticated `invitation_info` endpoint).
Pitch hard-constraint 1: no contributor email reaches the org before
acceptance.
- **Anti-oracle guard.** Only *visible contributors* of the project may
be invited (reusing the Community tab's member / deleted / disabled
exclusions, via a single shared `VISIBLE_CONTRIBUTOR` query). Every
other id — nonexistent, deleted, disabled, member, org member, or a
contributor of another project — returns one **byte-identical 404**, so
the endpoint leaks neither account existence nor state.
- **No permission-less membership.** `type = NONE` is rejected
server-side (`CANNOT_INVITE_CONTRIBUTOR_WITH_NO_PERMISSION`), not merely
hidden in the picker.
- Scoped/advanced invites route through the EE service exactly like the
email invite; `agencyId` stays null (a non-null agency would
short-circuit acceptance binding).
- Acceptance is bound to the invitee (`email = user.username`); a
different user gets `INVITATION_EMAIL_MISMATCH`.

## Frontend

- `members.edit`-gated **"Invite as member"** button on the contributor
row, opening the existing `PermissionsModal` picker with `hideNone`
(matching the email dialog). The permission→body mapping is factored
into a shared helper used by both invite hooks.

## Tests

- Backend (`InviteContributorControllerTest`,
`InviteContributorEeTest`): email stored + hidden, model +
public-endpoint email null, `MEMBERS_EDIT` gate, uniform-404 for every
non-visible-contributor id, re-invite dedup, NONE rejection, EE scoped
routing + granted languages, nameless-contributor path, acceptance
binding + granted membership.
- Frontend: vitest over the shared permission-body helper (all
role/scope/language branches, incl. scope-based filtering); an E2E case
that invites a contributor from the Community tab (typed `data-cy`
selectors), asserts the request succeeds, that a pending invitation
appears, and that the `NONE` role is absent from the picker.

## Notes

- Accepted, documented limitations (see the plan's Risks): an
already-invited contributor stays listed with a live button until they
accept (a second click fails safely with a no-email error), and
acceptance is email-bound rather than user-FK-bound (a full user-FK
model is deferred to a follow-up across all invitation kinds).
Anty0 and others added 6 commits July 26, 2026 14:15
A revision with no activity type and no modified entities carries no
information and renders as an empty UNKNOWN row. The store guard only
dropped such empties when the revision was typed; a null-typed revision
fell through `shouldSaveWithoutModification()` (which returned true for a
null type) and was persisted. Treat a null type like a default type so
these no-op revisions are not stored.
…oint

Adds per-project contributor tracking derived from the activity log: a
`project_contributor` cache table maintained by an AFTER-INSERT trigger on
`activity_revision` (mirroring the log — no foreign keys, backfilled once for
history), plus a paged, MEMBERS_VIEW-gated, email-free
`GET /v2/projects/{projectId}/contributors` endpoint that lists non-member
contributors and excludes current members, deleted and disabled users.

Part of pitch #3806 (Community translation v1.1 — Contributors), scope A.
…entry (#3820)

Add the "My contributions only" toggle to /community-projects (default on; lists public projects the user contributed to as a non-member, off lists all public projects) via an opt-in filterContributed param on GET /v2/public/projects/with-stats. Re-wire the "Community translation" org-switcher entry, gated on a new InitialDataModel.hasCommunityContributions flag; the gate and the list filter share one NON_MEMBER_CONTRIBUTOR_FILTER constant. Re-enable the five skipped switcher-navigation e2e tests and add default-on/negative-branch coverage.
Adds a Community tab to the project Members page listing non-member contributors
(avatar, name, first/last contribution date), email-free and gated behind
MEMBERS_VIEW; the tab bar appears only when the project is public and has >=1
contributor. Consumes the existing GET /v2/projects/{projectId}/contributors
endpoint. Includes a Cypress e2e spec + e2e data controller, and fixes
cleanTestData to tear down deleted/disabled fixture users.

Part of pitch #3806 (Community translation v1.1 — Contributors), scope B.
Part of pitch #3806 (Community translation v1.1 — Contributors), **scope
D**. Targets the integration branch (PR #3819), not `main`.

## What

Adds an **"Invite as member"** button to a Community-tab row on the
project Members page. Clicking it invites that contributor to the
project **by user id** — the server resolves their address, sends the
invitation mail, and never returns it to the org.

- New endpoint `PUT /v2/projects/{projectId}/invite-contributor`
(`MEMBERS_EDIT`); the body carries `userId` + permission fields, **no
email**.
- **Email-free by construction.** The address is stored server-side
(needed to send the mail and bind acceptance) but is masked at read time
by `Invitation.visibleEmail` — the single home of the rule — which
**every** assembler that serializes an invitee address reads (Team-tab
pending list *and* the unauthenticated `invitation_info` endpoint).
Pitch hard-constraint 1: no contributor email reaches the org before
acceptance.
- **Anti-oracle guard.** Only *visible contributors* of the project may
be invited (reusing the Community tab's member / deleted / disabled
exclusions, via a single shared `VISIBLE_CONTRIBUTOR` query). Every
other id — nonexistent, deleted, disabled, member, org member, or a
contributor of another project — returns one **byte-identical 404**, so
the endpoint leaks neither account existence nor state.
- **No permission-less membership.** `type = NONE` is rejected
server-side (`CANNOT_INVITE_CONTRIBUTOR_WITH_NO_PERMISSION`), not merely
hidden in the picker.
- Scoped/advanced invites route through the EE service exactly like the
email invite; `agencyId` stays null (a non-null agency would
short-circuit acceptance binding).
- Acceptance is bound to the invitee (`email = user.username`); a
different user gets `INVITATION_EMAIL_MISMATCH`.

## Frontend

- `members.edit`-gated **"Invite as member"** button on the contributor
row, opening the existing `PermissionsModal` picker with `hideNone`
(matching the email dialog). The permission→body mapping is factored
into a shared helper used by both invite hooks.

## Tests

- Backend (`InviteContributorControllerTest`,
`InviteContributorEeTest`): email stored + hidden, model +
public-endpoint email null, `MEMBERS_EDIT` gate, uniform-404 for every
non-visible-contributor id, re-invite dedup, NONE rejection, EE scoped
routing + granted languages, nameless-contributor path, acceptance
binding + granted membership.
- Frontend: vitest over the shared permission-body helper (all
role/scope/language branches, incl. scope-based filtering); an E2E case
that invites a contributor from the Community tab (typed `data-cy`
selectors), asserts the request succeeds, that a pending invitation
appears, and that the `NONE` role is absent from the picker.

## Notes

- Accepted, documented limitations (see the plan's Risks): an
already-invited contributor stays listed with a live button until they
accept (a second click fails safely with a no-email error), and
acceptance is email-bound rather than user-FK-bound (a full user-FK
model is deferred to a follow-up across all invitation kinds).
…rs list (#3823)

Strip a user's `username` (which is their e-mail) from every API response except the
surfaces where disclosure is legitimate: after this change one user can see another's
`username` only where the caller holds `MEMBERS_VIEW` AND the target is a current member
(plus org-members list, self, instance-admin, and the admin-only export ZIP).

`SecurityService.maskedMemberField` gated only on the caller's `MEMBERS_VIEW`, never on
target membership, and failed open off-request/off-project — so project admins on public
projects saw community contributors' and ex-members' raw e-mails, and non-project-scoped
endpoints leaked to anyone. Stripping the field makes the rule hold structurally instead
of re-checking membership on ~11 author-bearing surfaces.

`username` is now a deprecated always-empty field on the stripped models (kept on the wire
so clients don't break); a classpath-scanning `UsernameDisclosureGuard` test fails CI if any
non-sanctioned response model reintroduces it. Frontend already prefers name + avatar for
identity off the members/org/admin surfaces.
@Anty0
Anty0 force-pushed the jirikuchynka/community-contributors branch from 34a4bce to b01fbe2 Compare July 26, 2026 12:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
webapp/src/views/projects/members/component/getInvitationPermissionBody.test.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the tg.views alias for this module.

./getInvitationPermissionBody bypasses the required Tolgee path aliases.

Proposed fix
-import { getInvitationPermissionBody } from './getInvitationPermissionBody';
+import { getInvitationPermissionBody } from 'tg.views/projects/members/component/getInvitationPermissionBody';

As per coding guidelines, “Use Tolgee custom TypeScript path aliases (tg.component, tg.service, tg.hooks, tg.views, tg.globalContext) instead of relative imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@webapp/src/views/projects/members/component/getInvitationPermissionBody.test.ts`
at line 2, Update the import of getInvitationPermissionBody in the test to use
the tg.views path alias instead of the relative ./getInvitationPermissionBody
path, preserving the existing imported symbol and behavior.

Source: Coding guidelines

webapp/src/views/projects/members/component/useCreateInvitation.tsx (1)

3-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the Tolgee path alias for the shared helper.

Replace the new relative import with tg.views/projects/members/component/getInvitationPermissionBody. As per coding guidelines, “Use Tolgee custom TypeScript path aliases … instead of relative imports.”

Proposed fix
-import { getInvitationPermissionBody } from './getInvitationPermissionBody';
+import { getInvitationPermissionBody } from 'tg.views/projects/members/component/getInvitationPermissionBody';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/views/projects/members/component/useCreateInvitation.tsx` around
lines 3 - 6, Update the getInvitationPermissionBody import in
useCreateInvitation.tsx to use the Tolgee
tg.views/projects/members/component/getInvitationPermissionBody path alias
instead of the relative path, leaving the other imports unchanged.

Source: Coding guidelines

webapp/src/views/projects/members/component/useUpdatePermissions.ts (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a Tolgee path alias instead of a relative import.

This new relative import violates the webapp convention. Import permissionBody through the appropriate tg.views/... alias.

As per coding guidelines, webapp TypeScript files must use Tolgee custom TypeScript path aliases instead of relative imports.

Proposed import
-import { languagePermissionsForRole } from './permissionBody';
+import { languagePermissionsForRole } from 'tg.views/projects/members/component/permissionBody';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/views/projects/members/component/useUpdatePermissions.ts` at line
4, Update the import of languagePermissionsForRole in useUpdatePermissions.ts to
use the established tg.views/... Tolgee path alias for permissionBody instead of
the relative './permissionBody' path.

Source: Coding guidelines

webapp/src/component/PermissionsSettings/hierarchyTools.ts (1)

122-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Place the new caller before getScopeLanguagePermission.

scopesToLanguagePermissions calls getScopeLanguagePermission, but the caller is declared below the callee. Move the new helper above it to follow the required Stepdown ordering.

As per path instructions, functions should be ordered so that a caller appears before the functions it calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/component/PermissionsSettings/hierarchyTools.ts` around lines 122
- 134, Move the scopesToLanguagePermissions helper above
getScopeLanguagePermission, preserving its implementation and exports, so the
caller appears before the function it invokes according to the required stepdown
ordering.

Source: Path instructions

webapp/src/views/projects/members/component/getInvitationPermissionBody.ts (1)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the required tg.views aliases. These local relative imports violate the webapp alias convention.

  • webapp/src/views/projects/members/component/getInvitationPermissionBody.ts#L8-L8: import permissionBody through the appropriate tg.views/... alias.
  • webapp/src/views/projects/members/component/useCreateContributorInvitation.ts#L3-L3: import getInvitationPermissionBody through the appropriate tg.views/... alias.

As per coding guidelines, “Use Tolgee custom TypeScript path aliases (tg.component, tg.service, tg.hooks, tg.views, tg.globalContext) instead of relative imports.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/views/projects/members/component/getInvitationPermissionBody.ts`
at line 8, Replace the relative import in getInvitationPermissionBody.ts at line
8 with the appropriate tg.views alias for permissionBody, and replace the
relative getInvitationPermissionBody import in useCreateContributorInvitation.ts
at line 3 with its tg.views alias. Preserve the imported symbols and behavior.

Source: Coding guidelines

webapp/src/views/projects/members/component/ContributorItem.tsx (1)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the configured Tolgee path alias.

Replace the relative hook import with its tg.views alias.

Proposed fix
-import { useCreateContributorInvitation } from './useCreateContributorInvitation';
+import { useCreateContributorInvitation } from 'tg.views/projects/members/component/useCreateContributorInvitation';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/views/projects/members/component/ContributorItem.tsx` at line 17,
Update the useCreateContributorInvitation import in ContributorItem.tsx to use
the configured tg.views path alias instead of the relative path, leaving the
hook usage unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/api/src/main/kotlin/io/tolgee/websocket/ActivityWebsocketListener.kt`:
- Line 56: Update the actor payload construction in ActivityWebsocketListener
around simpleUserAccountModelAssembler.toModel(user) to redact the username
before sending websocket events, restoring the previous copy(username = "")
behavior or using an explicitly allowlisted actor DTO while preserving the other
assembled fields.

In
`@backend/app/src/test/kotlin/io/tolgee/service/project/ProjectHardDeletingServiceTest.kt`:
- Around line 176-189: Replace the native SQL contributor insert in
ProjectHardDeletingServiceTest with the appropriate contributor TestData
fixture, linking it to the existing project and user from BaseTestData. Persist
the fixture through testDataService alongside testData.root, and remove the
executeInNewTransaction/native-query setup.

In `@backend/data/src/test/kotlin/io/tolgee/unit/ShouldExposeMemberCountTest.kt`:
- Around line 13-23: Update the test `shouldExposeMemberCount is true
off-request without touching the project holder` to explicitly clear
`RequestContextHolder` before invoking `shouldExposeMemberCount()`, and restore
the previous request attributes afterward, using setup/teardown or a scoped
cleanup mechanism so thread-local state is preserved.

In
`@backend/testing/src/main/kotlin/io/tolgee/testing/AuthorizedControllerTest.kt`:
- Around line 195-210: Update recordProjectActivity to use the existing
setForcedDate helper when applying at, so any existing JWT is refreshed, and
restore the prior forced-date state after the transaction completes. Ensure
restoration occurs even when executeInNewTransaction or persistence fails, using
the existing date helpers rather than mutating currentDateProvider.forcedDate
directly.

In `@e2e/cypress/e2e/translations/softDeleteKeys.cy.ts`:
- Line 229: Replace the text-based selector in the user-selection flow with a
unique data-cy target keyed to the user ID, add that attribute to the rendered
user element if needed, and select it through cy.gcy().

---

Nitpick comments:
In `@webapp/src/component/PermissionsSettings/hierarchyTools.ts`:
- Around line 122-134: Move the scopesToLanguagePermissions helper above
getScopeLanguagePermission, preserving its implementation and exports, so the
caller appears before the function it invokes according to the required stepdown
ordering.

In `@webapp/src/views/projects/members/component/ContributorItem.tsx`:
- Line 17: Update the useCreateContributorInvitation import in
ContributorItem.tsx to use the configured tg.views path alias instead of the
relative path, leaving the hook usage unchanged.

In
`@webapp/src/views/projects/members/component/getInvitationPermissionBody.test.ts`:
- Line 2: Update the import of getInvitationPermissionBody in the test to use
the tg.views path alias instead of the relative ./getInvitationPermissionBody
path, preserving the existing imported symbol and behavior.

In `@webapp/src/views/projects/members/component/getInvitationPermissionBody.ts`:
- Line 8: Replace the relative import in getInvitationPermissionBody.ts at line
8 with the appropriate tg.views alias for permissionBody, and replace the
relative getInvitationPermissionBody import in useCreateContributorInvitation.ts
at line 3 with its tg.views alias. Preserve the imported symbols and behavior.

In `@webapp/src/views/projects/members/component/useCreateInvitation.tsx`:
- Around line 3-6: Update the getInvitationPermissionBody import in
useCreateInvitation.tsx to use the Tolgee
tg.views/projects/members/component/getInvitationPermissionBody path alias
instead of the relative path, leaving the other imports unchanged.

In `@webapp/src/views/projects/members/component/useUpdatePermissions.ts`:
- Line 4: Update the import of languagePermissionsForRole in
useUpdatePermissions.ts to use the established tg.views/... Tolgee path alias
for permissionBody instead of the relative './permissionBody' path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cbe401ce-679e-4ecb-84cf-1b7bc0b1d873

📥 Commits

Reviewing files that changed from the base of the PR and between 70a1fe0 and b01fbe2.

⛔ Files ignored due to path filters (2)
  • webapp/src/service/apiSchema.generated.ts is excluded by !**/*.generated.*
  • webapp/src/service/apiSchemaTypes.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (127)
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/InitialDataController.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectStatsController.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2InvitationController.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectContributorsController.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/PublicProjectsController.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/activity/ProjectActivityAuthorModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/activity/ProjectActivityModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/apiKey/ApiKeyModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/apiKey/ApiKeyModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/apiKey/ApiKeyWithLanguagesModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/apiKey/IApiKeyModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/apiKey/RevealedApiKeyModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/initialData/InitialDataModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/OrganizationInvitationModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/PublicInvitationModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/key/trash/TrashedKeyModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/key/trash/TrashedKeyWithTranslationsModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/translations/TranslationHistoryModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/translations/suggestions/TranslationSuggestionSimpleModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/SimpleUserAccountModel.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/SimpleUserAccountModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/websocket/ActivityWebsocketListener.kt
  • backend/app/src/test/kotlin/io/tolgee/activity/ActivityLogTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ApiKeyControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/CommunityPermissionTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/InitialDataControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/InviteContributorControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/NullTypedActivityRevisionStorageTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorBackfillTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorsControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PublicProjectsControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/V2UserControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/administration/AdministrationControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/notification/NotificationControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/TranslationCommentControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerHistoryTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2KeyController/KeyTrashFilterTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2KeyController/KeyTrashPermissionsTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/v2ProjectsController/ProjectsControllerTest.kt
  • backend/app/src/test/kotlin/io/tolgee/contributors/ContributorActivityRecorder.kt
  • backend/app/src/test/kotlin/io/tolgee/hateoas/UsernameDisclosureGuardCoreTest.kt
  • backend/app/src/test/kotlin/io/tolgee/hateoas/UsernameDisclosureGuardValueTest.kt
  • backend/app/src/test/kotlin/io/tolgee/service/UserAccountServiceTest.kt
  • backend/app/src/test/kotlin/io/tolgee/service/project/ProjectHardDeletingServiceTest.kt
  • backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt
  • backend/data/src/main/kotlin/io/tolgee/activity/projectActivity/ActivityViewByRevisionsProvider.kt
  • backend/data/src/main/kotlin/io/tolgee/api/IProjectActivityAuthorModel.kt
  • backend/data/src/main/kotlin/io/tolgee/api/UsernameFieldDeprecation.kt
  • backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/TestDataService.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/CommunityContributionE2eData.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ContributorsTestData.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/PublicProjectsE2eData.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/SoftDeleteKeysTestData.kt
  • backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateProjectInvitationParams.kt
  • backend/data/src/main/kotlin/io/tolgee/dtos/queryResults/TranslationHistoryView.kt
  • backend/data/src/main/kotlin/io/tolgee/dtos/request/project/ProjectInviteContributorDto.kt
  • backend/data/src/main/kotlin/io/tolgee/dtos/response/UserResponseDTO.kt
  • backend/data/src/main/kotlin/io/tolgee/model/Invitation.kt
  • backend/data/src/main/kotlin/io/tolgee/model/UserAccount.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributor.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributorId.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/KeyWithTranslationsView.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/ProjectContributorView.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/TranslationSuggestionView.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/activity/ProjectActivityView.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/KeyRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectContributorRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/ProjectRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/activity/ActivityModifiedEntityRepository.kt
  • backend/data/src/main/kotlin/io/tolgee/service/contributor/ProjectContributorService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/invitation/InvitationService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/key/KeySearchResultView.kt
  • backend/data/src/main/kotlin/io/tolgee/service/project/ProjectHardDeletingService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/project/ProjectService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/projectExportImport/ProjectExportImportPolicyRegistry.kt
  • backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryBase.kt
  • backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt
  • backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt
  • backend/data/src/main/resources/db/changelog/projectContributorBackfill.sql
  • backend/data/src/main/resources/db/changelog/projectContributorTrigger.sql
  • backend/data/src/main/resources/db/changelog/schema.xml
  • backend/data/src/test/kotlin/io/tolgee/unit/MemberInfoMaskingTest.kt
  • backend/data/src/test/kotlin/io/tolgee/unit/ShouldExposeMemberCountTest.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/AbstractE2eDataController.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/CommunityContributionE2eDataController.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/MembersCommunityE2eDataController.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/PublicProjectsE2eDataController.kt
  • backend/testing/src/main/kotlin/io/tolgee/testing/AuthorizedControllerTest.kt
  • backend/testing/src/main/kotlin/io/tolgee/testing/security/UsernameDisclosureGuard.kt
  • e2e/cypress/common/apiCalls/testData/testData.ts
  • e2e/cypress/e2e/projects/communityMembersTab.cy.ts
  • e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts
  • e2e/cypress/e2e/translations/softDeleteKeys.cy.ts
  • e2e/cypress/support/dataCyType.d.ts
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/component/TaskReportHelper.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TranslationSuggestionRepository.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/CommunitySuggestionTest.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/InviteContributorEeTest.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/component/TaskReportHelperTest.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/hateoas/UsernameDisclosureGuardEeTest.kt
  • webapp/src/component/PermissionsSettings/PermissionsModal.tsx
  • webapp/src/component/PermissionsSettings/PermissionsRole.tsx
  • webapp/src/component/PermissionsSettings/hierarchyTools.ts
  • webapp/src/component/common/UserName.tsx
  • webapp/src/component/organizationSwitch/OrganizationSwitch.tsx
  • webapp/src/globalContext/helpers.tsx
  • webapp/src/views/organizations/components/useUpdateBasePermissions.ts
  • webapp/src/views/projects/CommunityProjectsView.tsx
  • webapp/src/views/projects/DashboardProjectListItem.tsx
  • webapp/src/views/projects/MyContributionsToggle.tsx
  • webapp/src/views/projects/members/ProjectMembersView.tsx
  • webapp/src/views/projects/members/component/ContributorItem.tsx
  • webapp/src/views/projects/members/component/InvitationItem.tsx
  • webapp/src/views/projects/members/component/InviteDialog.tsx
  • webapp/src/views/projects/members/component/getInvitationPermissionBody.test.ts
  • webapp/src/views/projects/members/component/getInvitationPermissionBody.ts
  • webapp/src/views/projects/members/component/permissionBody.ts
  • webapp/src/views/projects/members/component/useCreateContributorInvitation.ts
  • webapp/src/views/projects/members/component/useCreateInvitation.tsx
  • webapp/src/views/projects/members/component/useUpdatePermissions.ts
  • webapp/src/views/projects/usePublicProjectsList.ts
💤 Files with no reviewable changes (15)
  • backend/data/src/main/kotlin/io/tolgee/service/key/KeySearchResultView.kt
  • backend/data/src/main/kotlin/io/tolgee/activity/projectActivity/ActivityViewByRevisionsProvider.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/apiKey/ApiKeyModelAssembler.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/TranslationSuggestionView.kt
  • backend/data/src/main/kotlin/io/tolgee/dtos/queryResults/TranslationHistoryView.kt
  • backend/data/src/main/kotlin/io/tolgee/dtos/response/UserResponseDTO.kt
  • backend/data/src/main/kotlin/io/tolgee/model/views/activity/ProjectActivityView.kt
  • backend/data/src/test/kotlin/io/tolgee/unit/MemberInfoMaskingTest.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/translations/TranslationHistoryModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/activity/ProjectActivityModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/SimpleUserAccountModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/key/trash/TrashedKeyModelAssembler.kt
  • backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryBase.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/translations/suggestions/TranslationSuggestionSimpleModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/key/trash/TrashedKeyWithTranslationsModelAssembler.kt
🚧 Files skipped from review as they are similar to previous changes (36)
  • backend/data/src/main/kotlin/io/tolgee/model/views/ProjectContributorView.kt
  • backend/app/src/test/kotlin/io/tolgee/contributors/ContributorActivityRecorder.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModel.kt
  • backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/PublicProjectsController.kt
  • backend/data/src/main/resources/db/changelog/projectContributorBackfill.sql
  • backend/api/src/main/kotlin/io/tolgee/hateoas/contributor/ContributorModelAssembler.kt
  • backend/api/src/main/kotlin/io/tolgee/hateoas/initialData/InitialDataModel.kt
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributor.kt
  • e2e/cypress/common/apiCalls/testData/testData.ts
  • backend/data/src/main/kotlin/io/tolgee/model/contributor/ProjectContributorId.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/CommunityContributionE2eDataController.kt
  • backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/PublicProjectsE2eData.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/MembersCommunityE2eDataController.kt
  • webapp/src/globalContext/helpers.tsx
  • backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt
  • webapp/src/views/projects/CommunityProjectsView.tsx
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/NullTypedActivityRevisionStorageTest.kt
  • backend/data/src/main/kotlin/io/tolgee/service/projectExportImport/ProjectExportImportPolicyRegistry.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/AbstractE2eDataController.kt
  • backend/data/src/main/kotlin/io/tolgee/service/contributor/ProjectContributorService.kt
  • backend/data/src/main/resources/db/changelog/projectContributorTrigger.sql
  • webapp/src/views/projects/MyContributionsToggle.tsx
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/TestDataService.kt
  • backend/app/src/test/kotlin/io/tolgee/service/UserAccountServiceTest.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/ContributorsTestData.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/PublicProjectsControllerTest.kt
  • backend/development/src/main/kotlin/io/tolgee/controllers/internal/e2eData/PublicProjectsE2eDataController.kt
  • webapp/src/views/projects/members/ProjectMembersView.tsx
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/InitialDataControllerTest.kt
  • backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/CommunityContributionE2eData.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorBackfillTest.kt
  • backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/ProjectContributorsControllerTest.kt
  • e2e/cypress/e2e/projects/communityProjectsNavigation.cy.ts
  • backend/data/src/main/kotlin/io/tolgee/service/project/ProjectService.kt

Comment on lines +176 to +189
val testData = BaseTestData()
testDataService.saveTestData(testData.root)
val projectId = testData.projectBuilder.self.id
val userId = testData.user.id

executeInNewTransaction(platformTransactionManager) {
entityManager
.createNativeQuery(
"insert into project_contributor (project_id, user_id, first_contribution_at, last_contribution_at) " +
"values (:projectId, :userId, now(), now())",
).setParameter("projectId", projectId)
.setParameter("userId", userId)
.executeUpdate()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use a TestData fixture for the contributor row.

Line 181 seeds test state through native SQL, bypassing the required TestData lifecycle. Represent this setup through a contributor TestData fixture and persist it with testDataService.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/app/src/test/kotlin/io/tolgee/service/project/ProjectHardDeletingServiceTest.kt`
around lines 176 - 189, Replace the native SQL contributor insert in
ProjectHardDeletingServiceTest with the appropriate contributor TestData
fixture, linking it to the existing project and user from BaseTestData. Persist
the fixture through testDataService alongside testData.root, and remove the
executeInNewTransaction/native-query setup.

Source: Coding guidelines

Comment on lines +13 to +23
@Test
fun `shouldExposeMemberCount is true off-request without touching the project holder`() {
val projectHolder =
mock<ProjectHolder> {
whenever(it.projectOrNull).thenThrow(IllegalStateException("project holder must not be read off-request"))
}
val securityService =
SecurityService(mock(), mock(), mock(), projectHolder = projectHolder, branchService = mock())

assertThat(securityService.shouldExposeMemberCount()).isTrue()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Isolate the off-request test from inherited request state.

Clear RequestContextHolder before the assertion and restore it afterward; otherwise a prior test on the same thread can invalidate this test’s premise.

Proposed fix
   fun `shouldExposeMemberCount is true off-request without touching the project holder`() {
+    val previousAttributes = RequestContextHolder.getRequestAttributes()
+    RequestContextHolder.resetRequestAttributes()
+    try {
     val projectHolder =
       mock<ProjectHolder> {
         whenever(it.projectOrNull).thenThrow(IllegalStateException("project holder must not be read off-request"))
       }
     val securityService =
       SecurityService(mock(), mock(), mock(), projectHolder = projectHolder, branchService = mock())
 
     assertThat(securityService.shouldExposeMemberCount()).isTrue()
+    } finally {
+      if (previousAttributes == null) RequestContextHolder.resetRequestAttributes()
+      else RequestContextHolder.setRequestAttributes(previousAttributes)
+    }
   }
📝 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.

Suggested change
@Test
fun `shouldExposeMemberCount is true off-request without touching the project holder`() {
val projectHolder =
mock<ProjectHolder> {
whenever(it.projectOrNull).thenThrow(IllegalStateException("project holder must not be read off-request"))
}
val securityService =
SecurityService(mock(), mock(), mock(), projectHolder = projectHolder, branchService = mock())
assertThat(securityService.shouldExposeMemberCount()).isTrue()
}
`@Test`
fun `shouldExposeMemberCount is true off-request without touching the project holder`() {
val previousAttributes = RequestContextHolder.getRequestAttributes()
RequestContextHolder.resetRequestAttributes()
try {
val projectHolder =
mock<ProjectHolder> {
whenever(it.projectOrNull).thenThrow(IllegalStateException("project holder must not be read off-request"))
}
val securityService =
SecurityService(mock(), mock(), mock(), projectHolder = projectHolder, branchService = mock())
assertThat(securityService.shouldExposeMemberCount()).isTrue()
} finally {
if (previousAttributes == null) RequestContextHolder.resetRequestAttributes()
else RequestContextHolder.setRequestAttributes(previousAttributes)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/data/src/test/kotlin/io/tolgee/unit/ShouldExposeMemberCountTest.kt`
around lines 13 - 23, Update the test `shouldExposeMemberCount is true
off-request without touching the project holder` to explicitly clear
`RequestContextHolder` before invoking `shouldExposeMemberCount()`, and restore
the previous request attributes afterward, using setup/teardown or a scoped
cleanup mechanism so thread-local state is preserved.

Comment on lines +195 to +210
protected fun recordProjectActivity(
authorId: Long?,
projectId: Long,
at: Date? = null,
) {
at?.let { currentDateProvider.forcedDate = it }
executeInNewTransaction {
entityManager.persist(
ActivityRevision().apply {
this.projectId = projectId
this.authorId = authorId
},
)
entityManager.flush()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the test clock through the existing date helpers.

Line 200 mutates the shared clock without restoring it, so later setup or authenticated requests can inherit a stale timestamp. It also bypasses setForcedDate, which refreshes an existing JWT.

Proposed fix
   protected fun recordProjectActivity(
     authorId: Long?,
     projectId: Long,
     at: Date? = null,
   ) {
-    at?.let { currentDateProvider.forcedDate = it }
-    executeInNewTransaction {
-      entityManager.persist(
-        ActivityRevision().apply {
-          this.projectId = projectId
-          this.authorId = authorId
-        },
-      )
-      entityManager.flush()
+    val previousForcedDate = currentDateProvider.forcedDate
+    try {
+      at?.let { setForcedDate(it) }
+      executeInNewTransaction {
+        entityManager.persist(
+          ActivityRevision().apply {
+            this.projectId = projectId
+            this.authorId = authorId
+          },
+        )
+        entityManager.flush()
+      }
+    } finally {
+      previousForcedDate?.let { setForcedDate(it) } ?: clearForcedDate()
     }
   }
📝 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.

Suggested change
protected fun recordProjectActivity(
authorId: Long?,
projectId: Long,
at: Date? = null,
) {
at?.let { currentDateProvider.forcedDate = it }
executeInNewTransaction {
entityManager.persist(
ActivityRevision().apply {
this.projectId = projectId
this.authorId = authorId
},
)
entityManager.flush()
}
}
protected fun recordProjectActivity(
authorId: Long?,
projectId: Long,
at: Date? = null,
) {
val previousForcedDate = currentDateProvider.forcedDate
try {
at?.let { setForcedDate(it) }
executeInNewTransaction {
entityManager.persist(
ActivityRevision().apply {
this.projectId = projectId
this.authorId = authorId
},
)
entityManager.flush()
}
} finally {
previousForcedDate?.let { setForcedDate(it) } ?: clearForcedDate()
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/testing/src/main/kotlin/io/tolgee/testing/AuthorizedControllerTest.kt`
around lines 195 - 210, Update recordProjectActivity to use the existing
setForcedDate helper when applying at, so any existing JWT is refreshed, and
restore the prior forced-date state after the transaction completes. Ensure
restoration occurs even when executeInNewTransaction or persistence fails, using
the existing date helpers rather than mutating currentDateProvider.forcedDate
directly.

cy.gcy('translations-filter-select').click();
cy.gcy('submenu-item').contains('Deleted by').click();
cy.gcy('filter-item').contains(users[0].username).click();
cy.gcy('filter-item').contains(users[0].name).click();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the text-based Cypress selector.

Line 229 selects by rendered name; add a unique data-cy target (for example, keyed by user ID) and select it through cy.gcy().

As per coding guidelines, “STRICTLY use data-cy attributes for E2E selectors, never rely on text content; use typed helpers gcy() or cy.gcy()”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/cypress/e2e/translations/softDeleteKeys.cy.ts` at line 229, Replace the
text-based selector in the user-selection flow with a unique data-cy target
keyed to the user ID, add that attribute to the rendered user element if needed,
and select it through cy.gcy().

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant