#2846: AccessListPage.java

projectforge-wicket/src/main/java/org/projectforge/web/access/AccessListPage.java

Path: projectforge-wicket/src/main/java/org/projectforge/web/access/AccessListPage.java · Lines: 228 · Extends: AbstractListPage<AccessListForm, AccessDao, GroupTaskAccessDO>

Purpose: THE access rights management page — the central UI for configuring who (group) can do what (access type) on which task (structure element). Displays GroupTaskAccess entries in a Wicket DataTable with 4 custom columns: hierarchical task tree, group name, access types (sub-table), and recursive flag (checkmark). Admin-only wizard button for bulk access setup.

Source: GitHub

228 lines · 180 code · 35 comments · 13 blank
CommitMessage
9ebb885222016-07-18 Initial commit

Generic type architecture — the 3-type contract

public class AccessListPage extends AbstractListPage<
    AccessListForm,          // F — the Wicket form with filter/search fields
    AccessDao,               // D — the Data Access Object for CRUD
    GroupTaskAccessDO        // E — the entity/domain object being listed
> implements IListPageColumnsCreator<GroupTaskAccessDO>

ProjectForge's Wicket architecture uses a 3-type generic pattern for every list page. This is the RAD (Rapid Application Development) approach from the development guide (#site/_docs/development.adoc): "Derive ui classes from the base classes." By extending AbstractListPage with these three types, the page inherits:

The @ListPage(editPage = AccessEditPage.class) annotation on line 53 links this list page to its edit counterpart. When a user clicks a row, Wicket navigates to AccessEditPage with the entity ID. This is ProjectForge's own annotation — not standard Wicket.

The 4-column DataTable — each column is a mini-renderer

// COLUMN 1: Task — hierarchical tree path
new PropertyColumn("task.title") {
  @Override public void populateItem(...) {
    TaskDO task = access.getTask();
    WicketTaskFormatter.appendFormattedTask(cycle, buf, task, true, false);
    item.add(new ListSelectActionPanel(..., AccessEditPage.class, access.getId(), ...));
    addRowClick(item);  // Whole row clicks through to edit page
  }
}

// COLUMN 2: Group — simple text
new PropertyColumn("group.name")

// COLUMN 3: Recursive — checkmark icon or empty
new PropertyColumn("recursive") {
  @Override public void populateItem(...) {
    if (access.getRecursive()) item.add(new IconPanel(..., IconType.ACCEPT));
    else item.add(createInvisibleDummyComponent(...));  // Keep DOM structure
  }
}

// COLUMN 4: Access entries — EMBEDDED SUB-TABLE
new PropertyColumn("accessEntries") {
  @Override public void populateItem(...) {
    AccessTablePanel accessTablePanel = new AccessTablePanel(..., accessEntries);
    if (rowIndex == 0) accessTablePanel.setDrawHeader(true);  // Header on first row
    item.add(accessTablePanel);
  }
}

Column 1 — Task tree (lines 83-105): The most complex column. WicketTaskFormatter.appendFormattedTask() renders the full hierarchical path of the task (e.g., "ACME Inc. → Web Portal → Sprint 3") as a single formatted string. The ListSelectActionPanel wraps this in a clickable link to AccessEditPage. addRowClick(item) makes the ENTIRE row clickable, not just the task name.

Column 3 — Recursive flag (lines 110-125): Shows a green checkmark (IconType.ACCEPT) if the access is recursive (applies to all child tasks). Otherwise shows an invisible dummy component — not just empty. This is a Wicket pattern: removing a component from the hierarchy breaks the repeater's index mapping. The dummy preserves the component tree structure while rendering nothing visible.

Column 4 — Embedded AccessTablePanel (lines 126-143): Each row contains a NESTED sub-table showing individual access entries (read, write, delete, etc.) for that group/task combination. The header is drawn only on the FIRST row (rowIndex == 0) to avoid repeating column headers on every row. This is a list-within-a-list pattern — the outer list shows group/task pairs, each containing an inner list of access types.

Admin wizard — bulk access setup (lines 160-174)

@Override protected void init() {
  if (getAccessChecker().isLoggedInUserMemberOfAdminGroup()) {
    ContentMenuEntryPanel menuEntry = new ContentMenuEntryPanel(..., new Link() {
      public void onClick() {
        TaskWizardPage wizardPage = new TaskWizardPage(params);
        wizardPage.setReturnToPage(AccessListPage.this);
        setResponsePage(wizardPage);
      }
    }, getString("wizard"));
    addContentMenuEntry(menuEntry);
  }
}

The wizard button appears in the page's context menu ONLY for admin users. It navigates to TaskWizardPage — a multi-step wizard that bulk-creates access entries for a task hierarchy. This is the "Quick Start" feature documented in the user guide: instead of manually creating dozens of access entries per task, the wizard generates them automatically with templates (e.g., "employee" template gives read access to all child tasks). The setReturnToPage(AccessListPage.this) ensures the wizard returns here after completion.

Select/unselect — filter by task, group, or user (lines 179-217)

public void select(String property, Object selectedValue) {
  if ("taskId".equals(property)) {
    form.getSearchFilter().setTaskId((Long) selectedValue); refresh();
  } else if ("groupId".equals(property)) {
    form.getSearchFilter().setGroupId((Long) selectedValue); refresh();
  } else if ("userId".equals(property)) {
    form.getSearchFilter().setUserId((Long) selectedValue); refresh();
  } else {
    super.select(property, selectedValue);
  }
}

The select/unselect methods implement a callback pattern for ProjectForge's selector popups. When the user clicks "Select task" in the filter bar, a popup picker opens. When the user picks a task, the popup calls select("taskId", taskId) on this page. The method sets the filter and calls refresh() to reload the table. unselect clears the filter. The groupId branch has form.groupSelectPanel.getTextField().modelChanged() — notifies Wicket that the text field's model was changed externally, triggering re-rendering.

Key takeaways