diff --git a/app/resources/locales/en_US/information.po b/app/resources/locales/en_US/information.po
index 95efe2696..1f1938ee1 100644
--- a/app/resources/locales/en_US/information.po
+++ b/app/resources/locales/en_US/information.po
@@ -142,16 +142,16 @@ msgid "People.delete.cluster"
msgstr "Account on {0}"
msgid "People.delete.hard"
-msgstr "Performing a hard delete will physically remove all records from the database, including all Changelog Entries. This operation cannot be undone, except by restoring from a backup."
+msgstr "Performing a hard delete will physically remove the Person and associated records from the database, including all Changelog Entries. This operation cannot be undone except by restoring from a backup."
msgid "People.delete.history.actor"
-msgstr "{0} History Records(s) for which this Person is an Actor will be updated to have no Actor"
+msgstr "{0} History Records(s) for which this Person is an Actor will be updated to have no Actor."
msgid "People.delete.history.job"
-msgstr "{0} Job History Records(s) related to this Person will be updated to remove the link"
+msgstr "{0} Job History Records(s) related to this Person will be updated to remove the link."
msgid "People.delete.person"
-msgstr "Deleting this record will delete the complete Person record for {1} (Person ID {2}), including history"
+msgstr "Deleting this record will delete the complete Person record for {1} (Person ID {2}), including history."
msgid "People.delete.ei"
msgstr "External Identity {0} from {1} (Source Key: {2})"
@@ -160,28 +160,31 @@ msgid "People.delete.ei.legacy"
msgstr "Legacy External Identity {0}"
msgid "People.delete.notifications.actor"
-msgstr "{0} Notification(s) for which this Person is an Actor will be updated to have no Actor"
+msgstr "{0} Notification(s) for which this Person is an Actor will be updated to have no Actor."
msgid "People.delete.notifications.recipient"
-msgstr "{0} Notification(s) for which this Person is a Recipient will be updated"
+msgstr "{0} Notification(s) for which this Person is a Recipient will be updated."
msgid "People.delete.notifications.resolver"
-msgstr "{0} Notification(s) for which this Person is the Resolver will be updated to have no Resolver"
+msgstr "{0} Notification(s) for which this Person is the Resolver will be updated to have no Resolver."
msgid "People.delete.petitions.history.actor"
-msgstr "{0} Petition History Records(s) for which this Person is an Actor will be updated to have no Actor"
+msgstr "{0} Petition History Records(s) for which this Person is an Actor will be updated to have no Actor."
msgid "People.delete.petitions.petitioner"
-msgstr "{0} Petitions(s) for which this Person is the Petitioner will be updated to have no Petitioner"
+msgstr "{0} Petitions(s) for which this Person is the Petitioner will be updated to have no Petitioner."
msgid "People.delete.role"
msgstr "Person Role {0}: (COU: {1}, Affiliation: {2}, Title: {3})"
msgid "People.delete.roles.manager"
-msgstr "{0} Person Role(s) for which this Person is a Manager will be updated to have no Manager"
+msgstr "{0} Person Role(s) for which this Person is a Manager will be updated to have no Manager."
msgid "People.delete.roles.sponsor"
-msgstr "{0} Person Role(s) for which this Person is a Sponsor will be updated to have no Sponsor"
+msgstr "{0} Person Role(s) for which this Person is a Sponsor will be updated to have no Sponsor."
+
+msgid "People.delete.soft"
+msgstr "Performing a soft delete will mark the Person and associated records as "deleted" and hide them from the user interface."
msgid "petition.history"
msgstr "Petition History"
diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po
index 6e545cf5f..3b6f003ff 100644
--- a/app/resources/locales/en_US/operation.po
+++ b/app/resources/locales/en_US/operation.po
@@ -192,6 +192,9 @@ msgstr "Decline"
msgid "delete"
msgstr "Delete"
+msgid "delete.a"
+msgstr "Delete {0}?"
+
msgid "delete.confirm"
msgstr "Are you sure you want to delete this record ({0})?"
@@ -199,6 +202,9 @@ msgid "delete.confirm.title"
msgstr "Confirm Deletion"
msgid "delete.hard"
+msgstr "Hard Delete"
+
+msgid "delete.hard.perform"
msgstr "Perform Hard Delete"
msgid "delete.queue"
@@ -207,6 +213,9 @@ msgstr "Queue for Deletion"
msgid "delete.queue.confirm"
msgstr "Are you sure you want to schedule this record ({0}) for deletion?"
+msgid "delete.soft"
+msgstr "Soft Delete"
+
msgid "duplicate"
msgstr "Duplicate"
diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php
index f09080c69..cee093773 100644
--- a/app/src/Controller/Component/BreadcrumbComponent.php
+++ b/app/src/Controller/Component/BreadcrumbComponent.php
@@ -295,10 +295,10 @@ public function injectPrimaryLink(
$linkTable = \Cake\ORM\TableRegistry::getTableLocator()->get($linkModelFqn);
$contain = $this->resolveContainList($linkTable, $mappedAction);
- // Normalize attr (people_id → id when the attr matches the model’s own foreign key)
+ // Use the table alias for query building (avoid plugin-qualified names in SQL)
$modelAlias = $linkTable->getAlias();
- $foreignKey = StringUtilities::classNameToForeignKey($modelAlias);
- $linkAttr = ($link->attr === $foreignKey) ? 'id' : $link->attr;
+
+ $linkAttr = (string)$linkTable->getPrimaryKey();
// Fetch the linked entity; if not found, handle gracefully
$linkedEntity = $linkTable
diff --git a/app/src/Controller/PeopleController.php b/app/src/Controller/PeopleController.php
index 97152d9ec..3b71d4d03 100644
--- a/app/src/Controller/PeopleController.php
+++ b/app/src/Controller/PeopleController.php
@@ -147,9 +147,17 @@ public function confirmDelete(string $id) {
'PetitionHistoryRecords'
];
- $this->set('vv_person', $this->People->get((int)$id, contain: $contain));
+ $person = $this->People->get((int)$id, contain: $contain);
+ $this->set('vv_person', $person);
+
+ $this->Breadcrumb->injectTitleLink(
+ table: $this->People,
+ entity: $person,
+ action: 'edit',
+ label: $this->People->generateDisplayField($person)
+ );
- $this->set('vv_title', __d('operation', 'delete.confirm.title'));
+ $this->set('vv_title', __d('operation', 'delete.a',[$person->primary_name->full_name]));
// We pull cluster information separately because it can't be contain'd
$this->set('vv_person_cluster_status', $this->People->Cos->Clusters->status((int)$id));
diff --git a/app/templates/People/confirm_delete.php b/app/templates/People/confirm_delete.php
index 7829262e6..d372cd21c 100644
--- a/app/templates/People/confirm_delete.php
+++ b/app/templates/People/confirm_delete.php
@@ -32,19 +32,21 @@
-
= $vv_title ?>
+ = $vv_title ?>
= $this->element('flash') // Flash messages ?>
= $this->element('notify/alert', [
- 'message' => __d('operation', 'delete.confirm', [$vv_person->primary_name->full_name]),
- 'type' => 'information'
+ 'message' => __d('operation', 'delete.confirm', [$vv_person->id]),
+ 'type' => 'warning'
]) ?>
-
- - = __d(
+
+ -
+ info
+ = __d(
'information',
'People.delete.person',
[
@@ -52,113 +54,204 @@
$vv_person->primary_name->full_name,
$vv_person->id,
$this->Url->build(['controller' => 'history-records', 'action' => 'index', '?' => ['person_id' => $vv_person->id]])
- ]) ?>
+ ]) ?>
- person_roles)): ?>
-
- person_roles as $role): ?>
- - = __d(
- 'information',
- 'People.delete.role',
- [
- $role->id,
- $role->cou->name ?? '-',
- $role->affiliation_type->display_name,
- $role->title ?: '-'
- ]
- ) ?>
-
-
-
-
- external_identities)): ?>
-
- external_identities as $ei): ?>
- - =
- !empty($ei->ext_identity_source_record)
- ? __d(
- 'information',
- 'People.delete.ei',
- [
- $ei->id,
- $ei->ext_identity_source_record->external_identity_source->description,
- $ei->ext_identity_source_record->source_key
- ]
- )
- // Legacy External Identities do not have Source Records
- : __d('information', 'People.delete.ei.legacy', [$ei->id])
- ?>
-
-
-
-
-
-
-
- - = __d('information', 'People.delete.cluster', [$cs['cluster']->description]) ?>
-
-
-
+ person_roles)): ?>
+
+ person_roles as $role): ?>
+ - = __d(
+ 'information',
+ 'People.delete.role',
+ [
+ $role->id,
+ $role->cou->name ?? '-',
+ $role->affiliation_type->display_name,
+ $role->title ?: '-'
+ ]
+ ) ?>
+
+
+
+
+ external_identities)): ?>
+
+ external_identities as $ei): ?>
+ - =
+ !empty($ei->ext_identity_source_record)
+ ? __d(
+ 'information',
+ 'People.delete.ei',
+ [
+ $ei->id,
+ $ei->ext_identity_source_record->external_identity_source->description,
+ $ei->ext_identity_source_record->source_key
+ ]
+ )
+ // Legacy External Identities do not have Source Records
+ : __d('information', 'People.delete.ei.legacy', [$ei->id])
+ ?>
+
+
+
+
+
+
+
+ - = __d('information', 'People.delete.cluster', [$cs['cluster']->description]) ?>
+
+
+
+
sponsor_person_roles)): ?>
- - = __d('information', 'People.delete.roles.sponsor', [count($vv_person->sponsor_person_roles)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.roles.sponsor', [count($vv_person->sponsor_person_roles)]) ?>
+
manager_person_roles)): ?>
- - = __d('information', 'People.delete.roles.manager', [count($vv_person->manager_person_roles)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.roles.manager', [count($vv_person->manager_person_roles)]) ?>
+
actor_history_records)): ?>
- - = __d('information', 'People.delete.history.actor', [count($vv_person->actor_history_records)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.history.actor', [count($vv_person->actor_history_records)]) ?>
+
job_history_records)): ?>
- - = __d('information', 'People.delete.history.job', [count($vv_person->job_history_records)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.history.job', [count($vv_person->job_history_records)]) ?>
+
actor_notifications)): ?>
- - = __d('information', 'People.delete.notifications.actor', [count($vv_person->actor_notifications)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.notifications.actor', [count($vv_person->actor_notifications)]) ?>
+
recipient_notifications)): ?>
- - = __d('information', 'People.delete.notifications.recipient', [count($vv_person->recipient_notifications)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.notifications.recipient', [count($vv_person->recipient_notifications)]) ?>
+
resolver_notifications)): ?>
- - = __d('information', 'People.delete.notifications.resolver', [count($vv_person->resolver_notifications)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.notifications.resolver', [count($vv_person->resolver_notifications)]) ?>
+
petitioner_petitions)): ?>
- - = __d('information', 'People.delete.petitions.petitioner', [count($vv_person->petitioner_petitions)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.petitions.petitioner', [count($vv_person->petitioner_petitions)]) ?>
+
petition_history_records)): ?>
- - = __d('information', 'People.delete.petitions.history.actor', [count($vv_person->petition_history_records)]) ?>
+
-
+ info
+ = __d('information', 'People.delete.petitions.history.actor', [count($vv_person->petition_history_records)]) ?>
+
-Form->create($vv_person, [
+= // Begin the form
+ $this->Form->create($vv_person, [
'id' => 'person-confirm-delete',
'type' => 'post',
'url' => ['action' => 'delete', $vv_person->id]
]);
+?>
- print $this->element('notify/alert', [
+
+ = $this->element('notify/alert', [
+ 'message' => __d('information', 'People.delete.soft'),
+ 'type' => 'information'
+ ]); ?>
+
+
+ = $this->element('notify/alert', [
'message' => __d('information', 'People.delete.hard'),
- 'type' => 'warning'
- ]);
+ 'type' => 'danger'
+ ]); ?>
+
-// XXX is this going to be a standard supported by StandardController?
-// if so update developer documentation
- print $this->Form->checkbox('hard-delete') . "\n";
- print $this->Form->label('hard-delete', __d('operation', 'delete.hard')) . "\n";
+
+ = // This is the exposed button that the user clicks (but not the actual submit button)
+ $this->Form->button(__d('operation', 'delete.soft'),[
+ 'type' => 'button',
+ 'id' => 'person-delete-button',
+ 'class' => 'btn btn-primary',
+ 'data-bs-toggle' => 'modal',
+ 'data-bs-target' => '#dialog'
+ ]) . "\n"; ?>
- print $this->Form->submit(__d('operation', 'delete'), ['class' => 'btn-sm']) . "\n";
+ = // Checkbox to switch to hard delete (instead of soft delete)
+ $this->Form->control('hard-delete', [
+ 'type' => 'checkbox',
+ 'label' => __d('operation', 'delete.hard.perform'),
+ 'class' => 'form-check-input',
+ 'id' => 'hard-delete'
+ ]) . "\n"; ?>
- print $this->Form->end() . "\n";
-?>
+ = // This is the hidden submit button that is activated by our confirmation dialog.
+ $this->Form->submit(__d('operation', 'delete'), [
+ 'id' => 'delete-confirmed',
+ 'class' => 'invisible'
+ ]) . "\n"; ?>
+
+
+= $this->Form->end() . "\n"; ?>
+
+
diff --git a/app/templates/People/fields.inc b/app/templates/People/fields.inc
index ed7997ea5..3fc77a299 100644
--- a/app/templates/People/fields.inc
+++ b/app/templates/People/fields.inc
@@ -54,7 +54,7 @@ if($vv_action == 'add') {
// The initial name must be primary
'names.0.primary_name' => true
];
-} elseif($vv_action == 'edit') {
+} else {
$a = $vv_obj->extract(['person_role_id', 'person_id']);
$useDeleteSplashPage = true;
diff --git a/app/templates/Standard/add-edit-view.php b/app/templates/Standard/add-edit-view.php
index 53b0e90eb..0f197e01c 100644
--- a/app/templates/Standard/add-edit-view.php
+++ b/app/templates/Standard/add-edit-view.php
@@ -153,8 +153,22 @@
$perm = $vv_permissions[$linkModel][$t['link']['action']];
}
- // Inject a link to the current object ID
- $t['link']['?'][\App\Lib\Util\StringUtilities::entityToForeignKey($vv_obj)] = $vv_obj->id;
+ // This fixes the link in the gear icon in the Person Canvas view.
+ // Inject a link to the current object ID, unless an equivalent specific foreign key
+ // (like subject_person_id) is already provided in the query arguments.
+ $fk = \App\Lib\Util\StringUtilities::entityToForeignKey($vv_obj);
+ $hasFk = false;
+ if(!empty($t['link']['?'])) {
+ foreach($t['link']['?'] as $k => $v) {
+ if($v === $vv_obj->id && (str_ends_with($k, '_' . $fk) || $k === $fk)) {
+ $hasFk = true;
+ break;
+ }
+ }
+ }
+ if(!$hasFk) {
+ $t['link']['?'][$fk] = $vv_obj->id;
+ }
} else {
$perm = $vv_permissions[$t['link']['action']];
diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css
index 696489899..9b2db84e5 100644
--- a/app/webroot/css/co-base.css
+++ b/app/webroot/css/co-base.css
@@ -477,6 +477,7 @@ body.cos.select #top-bar {
margin-right: 0.25em;
}
.co-alert a {
+ color: var(--cmg-color-link-darker);
text-decoration: underline;
}
/* Alerts in the add-edit form: */
@@ -1485,6 +1486,36 @@ a.cm-row-button:focus {
border: 1px solid var(--cmg-color-btn-bg-001);
color: var(--cmg-color-txt-inverse) !important;
}
+#person-confirm-delete {
+ margin-left: 1em;
+}
+#person-confirm-delete .alert {
+ display: inline-block;
+}
+ul.delete-info-list {
+ margin: 0 1rem 1.5rem;
+ padding: 0;
+}
+ul.delete-info-list li {
+ margin-top: 1rem;
+ line-height: 1.75rem;
+ list-style: none;
+ font-family: var(--cmg-font-semibold);
+}
+ul.delete-info-list li:first-child {
+ margin-top: 0;
+}
+ul.delete-info-list li li {
+ margin-top: 0;
+ font-weight: 400;
+ list-style: circle;
+ font-family: var(--cmg-font-regular);
+}
+#content ul.delete-info-list .material-symbols-outlined {
+ font-size: 1.5rem;
+ margin-top: -4px;
+ vertical-align: middle;
+}
/* DATA LISTS */
ul.data-list {
padding-left: 0;
@@ -1588,6 +1619,11 @@ ul.form-list li.fields-submit {
ul.form-list li.fields-submit .field:hover {
background-color: var(--cmg-color-body-bg);
}
+.submit-with-flags {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
ul.form-list .field-name {
display: block;
}
diff --git a/app/webroot/css/co-variables.css b/app/webroot/css/co-variables.css
index 363206936..f749cf7e0 100644
--- a/app/webroot/css/co-variables.css
+++ b/app/webroot/css/co-variables.css
@@ -46,6 +46,7 @@
/* Links (primary) */
--cmg-color-link: #06c; /* primary hyperlink color (blue); accessible on backgrounds down to #e7e7e7 */
--cmg-color-link-hover: #115791; /* primary hyperlink hover color (blue); accessible on backgrounds down to #c9c9c9 */
+ --cmg-color-link-darker: #005EBD; /* slightly darker hyperlink color for use on alert backgrounds */
/* Buttons */
--cmg-color-btn-bg-001: #115791; /* primary button background color (blue), submit buttons, .btn-primary, pagination */