Files
bin/espocrm-tags-rowactions-unlink-only-claude-code.md
mkt cebe829dcd Add README, customization snapshot, and snapshot/restore tooling
Provisioning now restores all GUI customizations on reset+reprovision:

- create_pod_espocrm.sh: deploy the version-controlled espocrm-custom/ tree
  (CTag entity, layouts, i18n, clientDefs, custom views, custom CSS) into the
  pod, then chown www-data and rebuild. Replaces the earlier inline CSS-only
  step. Adds a live-phase cache rebuild so customizations and the client
  cacheTimestamp are refreshed on every run.
- espocrm-custom/: snapshot of custom/ and client/custom/ (source of truth).
- snapshot_espocrm_custom.sh: refresh the snapshot from a running pod.
- readme.md: usage, first-time host setup, image-update and reset workflows.
- Include the task/instruction notes and plan.md for reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:51:02 +02:00

9.0 KiB

Task: Reduce the Tags relationship-panel row menu to "Unlink" only in EspoCRM

Goal

This EspoCRM instance has a custom many-to-many relationship to a custom Tag entity, exposed as a "Tags" bottom relationship panel on three entity types: Opportunity, Contact, and Account. On each of these three entities the link is named cTags. (The inline Create (+) button on these panels was already removed in a previous task via relationshipPanels.cTags.create = false — leave that in place.)

On a record's Tags panel, each attached tag row has a small dropdown (triangle/▾) that currently offers four actions: View, Edit, Unlink, Remove (German UI: Ansehen, Bearbeiten, Link entfernen, Löschen).

We want each tag row's dropdown to offer only "Unlink" (German: "Link entfernen"). The actions View, Edit, and Remove/Delete must be gone. This must apply to all users, including regular (non-admin) users, on all three entities (Opportunity, Contact, Account).

Do not change anything else (no ACLs, no other panels/fields/entities, and do not touch the Tag entity itself — creating, viewing, editing and deleting tags from the Tag entity's own list/detail views must keep working normally).

Environment

  • EspoCRM (version 9.3.8, Apache variant) runs as a rootless Podman pod named espocrm_pod on this server. The web container is expected to be espocrm_ctr (confirm with podman ps).
  • EspoCRM application root inside the web container: /var/www/html. The app runs as user www-data.
  • Customizations live under /var/www/html/ and must be on the persisted bind-mount so they survive container recreation. Two locations are used here:
    • Backend metadata: /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/ (this already holds Opportunity.json, Contact.json, Account.json from the +button task).
    • Frontend custom view: /var/www/html/client/custom/src/ (this is the official location for custom client-side views; it is part of the same persisted /var/www/html that already keeps your custom/ metadata across restarts).

Mechanism (verified against EspoCRM 9.3.8 source)

The per-row dropdown of a relationship panel is rendered by the view views/record/row-actions/relationship. Its getActionList() builds the menu like this (paraphrased from client/src/views/record/row-actions/relationship.js):

  • View — pushed unconditionally (always present; there is no flag to disable it).
  • Edit — pushed only if this.options.acl.edit && !this.options.editDisabled.
  • Unlink — pushed only if !this.options.unlinkDisabled.
  • Remove — pushed only if this.options.acl.delete && !this.options.removeDisabled.

The panel (client/src/views/record/panels/relationship.js) feeds these flags from the panel metadata and lets you swap the whole row-actions view:

this.rowActionsView = this.defs.readOnly ? false : (this.defs.rowActionsView || this.rowActionsView);
// ...
rowActionsOptions: {
    unlinkDisabled: unlinkDisabled,
    editDisabled: this.defs.editDisabled,
    removeDisabled: this.defs.removeDisabled,
},

Consequence:

  • editDisabled: true and removeDisabled: true (in clientDefs.<Entity>.relationshipPanels.cTags) would hide Edit and Remove — but not View, because View is hard-coded.
  • To leave only Unlink, we point the panel's rowActionsView (a supported relationshipPanels.<link> parameter) at a small custom row-actions view that returns only the Unlink action. This removes View, Edit and Remove in one go and is the clean, upgrade-safe approach.

Custom client views live under client/custom/src/... and are referenced with the custom: prefix (per the official EspoCRM docs, "Custom views"). No build step is required; a cache rebuild plus a hard browser reload picks them up.

Confidence / self-verify before relying on it

The action name unlinkRelated, the four-action structure, and the rowActionsView parameter are taken from the 9.3.8 source. Before relying on them, confirm against this instance:

podman exec espocrm_ctr sh -lc "grep -n 'rowActionsView\|unlinkDisabled\|editDisabled\|removeDisabled' /var/www/html/client/src/views/record/panels/relationship.js"
podman exec espocrm_ctr sh -lc "grep -n 'quickView\|quickEdit\|unlinkRelated\|removeRelated' /var/www/html/client/src/views/record/row-actions/relationship.js"

You should see View pushed unconditionally, Unlink guarded by unlinkDisabled, and the panel reading this.defs.rowActionsView. If this version differs, adjust the custom view's getActionList() to match the real action name(s) for "Unlink".

Steps

  1. Confirm container and link name.

    • podman ps → identify the web container (expected espocrm_ctr).
    • Confirm each of Opportunity, Contact, Account has a many-to-many link named cTags:
      podman exec espocrm_ctr sh -lc "grep -rn 'cTags' /var/www/html/custom/Espo/Custom/Resources/metadata/entityDefs/"
      
  2. Create the custom row-actions view. Create the directory and file (inside the web container, on the persisted /var/www/html):

    /var/www/html/client/custom/src/views/record/row-actions/tags-unlink-only.js

    define('custom:views/record/row-actions/tags-unlink-only',
    ['views/record/row-actions/relationship'], (RelationshipRowActionsView) => {
    
        return class extends RelationshipRowActionsView {
    
            getActionList() {
                const list = [];
    
                // Keep only the "Unlink" action (German UI: "Link entfernen").
                // View / Edit / Remove are intentionally omitted.
                if (!this.options.unlinkDisabled) {
                    list.push({
                        action: 'unlinkRelated',
                        label: 'Unlink',
                        data: {
                            id: this.model.id,
                        },
                        groupIndex: 0,
                    });
                }
    
                // Preserve any explicitly configured extra row actions (none by default).
                this.getAdditionalActionList().forEach(item => list.push(item));
    
                return list;
            }
        };
    });
    

    Make sure the directories exist, e.g.:

    podman exec espocrm_ctr sh -lc "mkdir -p /var/www/html/client/custom/src/views/record/row-actions"
    
  3. Point the three Tags panels at the custom view. For Opportunity, Contact, and Account, add the rowActionsView key to the existing cTags panel in: /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/<EntityType>.json

    The resulting cTags block must look like this (note: keep the existing "create": false from the previous task):

    {
        "relationshipPanels": {
            "cTags": {
                "create": false,
                "rowActionsView": "custom:views/record/row-actions/tags-unlink-only"
            }
        }
    }
    
    • Target files:
      • /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Opportunity.json
      • /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Contact.json
      • /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs/Account.json
    • MERGE into the existing JSON. Preserve all other keys; in particular keep relationshipPanels.cTags.create = false. Only add the single rowActionsView key inside relationshipPanels.cTags. Do not overwrite the file. Produce valid JSON.
  4. Fix ownership so the web user owns the new/changed files (run as root inside the container):

    podman exec espocrm_ctr chown -R www-data:www-data /var/www/html/client/custom /var/www/html/custom/Espo/Custom/Resources/metadata/clientDefs
    
  5. Clear cache and rebuild (run as the web user):

    podman exec -u www-data espocrm_ctr php command.php rebuild
    

    If command.php is not present in this version, use:

    podman exec -u www-data espocrm_ctr bin/command rebuild
    

    (Alternatively, an admin can run Administration → Rebuild in the web UI.)

  6. Verify in the browser (do a hard reload, Ctrl+Shift+R, to drop the client cache; ideally test as a regular non-admin user, and also as admin):

    • Open an Opportunity, a Contact, and an Account record. On the Tags panel, open the ▾ dropdown of an attached tag. It must now show only "Link entfernen" (Unlink). Ansehen / Bearbeiten / Löschen (View / Edit / Remove) must be gone.
    • "Link entfernen" must still work (it detaches the tag from the record but does not delete the Tag itself).
    • The Tag entity's own list/detail views must still allow viewing, editing and deleting tags normally (this task does not touch the Tag entity or any ACL).

Rollback

  1. Remove the rowActionsView key from relationshipPanels.cTags in the three clientDefs files (leave create: false if you still want the +button hidden).
  2. Optionally delete /var/www/html/client/custom/src/views/record/row-actions/tags-unlink-only.js.
  3. Rebuild again (step 5) and hard-reload the browser.