AccessListPage.java| Commit | Message |
|---|---|
9ebb88522 | 2016-07-18 Initial commit |
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>// 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.
@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.
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.
<F, D, E> pattern. Understanding this unlocks the entire Wicket UI architecture.createInvisibleDummyComponent is a critical Wicket pattern — never return null or skip components in a repeater.AccessTablePanel inside a table cell. This is a powerful but expensive pattern — each row creates a new Wicket component tree.setReturnToPage(this) creates a navigation breadcrumb back to the list page.isLoggedInUserMemberOfAdminGroup(). Non-admin users never see it — the security is enforced at the UI level (in addition to server-side access checks in the DAO).
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
AbstractListPagewith these three types, the page inherits:AccessListForm(the F type). The form provides filter fields (task selector, group selector, user selector, search text).AccessDao(the D type). The DAO handles database queries with automatic access right filtering — users only see entries they're authorized to view.GroupTaskAccessDO(the E type). The entity provides column data, sorting, and row identification.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 toAccessEditPagewith the entity ID. This is ProjectForge's own annotation — not standard Wicket.