Compare commits
No commits in common. "39d96bfa180d355f3539f4d76499968fc0c81535" and "ff3dff9a9ae097d43e0f1b5aa88ec2befbc1c837" have entirely different histories.
39d96bfa18
...
ff3dff9a9a
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: FECA Mailshots
|
||||
* Plugin URI: https://fenedge.co.uk/
|
||||
* Description: FECA mailshots plugin.
|
||||
* Version: 1.0.29
|
||||
* Version: 1.0.27
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: FECA
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ final class DataSourcesAdminPage
|
|||
$this->wp->addAction('rest_api_init', [$this, 'registerRestRoutes']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_save', [$this, 'handleUiSave']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_delete', [$this, 'handleUiDelete']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_duplicate', [$this, 'handleUiDuplicate']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_validate', [$this, 'handleUiValidate']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_data_sources_ui_preview', [$this, 'handleUiPreview']);
|
||||
}
|
||||
|
|
@ -270,12 +269,6 @@ final class DataSourcesAdminPage
|
|||
echo '<td>' . htmlspecialchars((string) ($row['updated_at'] ?? '')) . '</td>';
|
||||
echo '<td><a class="button button-small" href="' . htmlspecialchars($editUrl) . '">Edit</a> ';
|
||||
echo '<form method="post" action="' . $action . '" class="feca-inline-form">';
|
||||
echo '<input type="hidden" name="action" value="feca_mailshots_data_sources_ui_duplicate">';
|
||||
echo $this->hiddenNonceField(self::NONCE_ACTION);
|
||||
echo '<input type="hidden" name="id" value="' . $id . '">';
|
||||
echo '<button type="submit" class="button button-small">Duplicate</button>';
|
||||
echo '</form> ';
|
||||
echo '<form method="post" action="' . $action . '" class="feca-inline-form">';
|
||||
echo '<input type="hidden" name="action" value="feca_mailshots_data_sources_ui_delete">';
|
||||
echo $this->hiddenNonceField(self::NONCE_ACTION);
|
||||
echo '<input type="hidden" name="id" value="' . $id . '">';
|
||||
|
|
@ -460,29 +453,6 @@ final class DataSourcesAdminPage
|
|||
}
|
||||
}
|
||||
|
||||
public function handleUiDuplicate(): void
|
||||
{
|
||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||
return;
|
||||
}
|
||||
$id = (int) ($this->wp->requestParam('id', '0') ?? '0');
|
||||
$result = ['ok' => false, 'errors' => ['Data source ID is required.']];
|
||||
try {
|
||||
if ($id > 0) {
|
||||
$result = $this->service()->duplicate($id);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$result = ['ok' => false, 'errors' => [$e->getMessage()]];
|
||||
}
|
||||
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
||||
$editId = !empty($result['ok']) && isset($result['id']) ? (int) $result['id'] : 0;
|
||||
$url = $this->wp->adminUrl('admin.php?page=feca-mailshot-data-sources' . ($editId > 0 ? '&edit_id=' . $editId : ''));
|
||||
if (!headers_sent()) {
|
||||
header('Location: ' . $url, true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleApi(): void
|
||||
{
|
||||
if (!$this->enforceCapabilityOrJson(self::CAPABILITY)) {
|
||||
|
|
@ -554,14 +524,6 @@ final class DataSourcesAdminPage
|
|||
$this->wp->sendJson(['ok' => true]);
|
||||
return;
|
||||
}
|
||||
if ($op === 'duplicate') {
|
||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||
return;
|
||||
}
|
||||
$id = (int) ($this->wp->requestParam('id', '0') ?? '0');
|
||||
$this->wp->sendJson($this->service()->duplicate($id));
|
||||
return;
|
||||
}
|
||||
$this->wp->sendJson(['ok' => false, 'error' => 'Unknown operation'], 400);
|
||||
} catch (\Throwable $e) {
|
||||
$this->wp->sendJson(['ok' => false, 'error' => $e->getMessage()], 500);
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ final class ReviewRecipientsAdminPage
|
|||
echo 'function sourceStorageKey(src){return storagePrefix+String(src||"");}';
|
||||
echo 'function loadSelection(src){if(!src){return {};}try{var raw=sessionStorage.getItem(sourceStorageKey(src));if(!raw){return {};}var parsed=JSON.parse(raw);if(parsed&&typeof parsed==="object"){return parsed;}}catch(_){ }return {};}';
|
||||
echo 'function saveSelection(){if(!state.source){return;}try{sessionStorage.setItem(sourceStorageKey(state.source),JSON.stringify(state.selectedKeys||{}));}catch(_){ }}';
|
||||
echo 'function detectKey(row){var r=row||{};if(r.__rr_index!==undefined){return "row_index:"+String(r.__rr_index);}return "";}';
|
||||
echo 'function detectKey(row){var r=row||{};var fields=["ID","id","Accountid","account_id"];for(var i=0;i<fields.length;i++){var f=fields[i];if(r[f]!==undefined&&r[f]!==null&&String(r[f]).trim()!==""){return String(r[f]).trim();}}if(r.Email!==undefined&&String(r.Email).trim()!==""){return String(r.Email).trim();}if(r.email!==undefined&&String(r.email).trim()!==""){return String(r.email).trim();}if(r.__rr_index!==undefined){return "row_index:"+String(r.__rr_index);}return "";}';
|
||||
echo 'function renderTable(){clearErr();var src=state.source;if(!src){setColumnWidths([]);headRow.innerHTML="<th>No recipients loaded.</th>";bodyEl.innerHTML="";status("Select a data source to load recipients.");rebuildSortColumns([], "");return;}var payload=cache[src];if(!payload){setColumnWidths([]);headRow.innerHTML="<th>No recipients loaded.</th>";bodyEl.innerHTML="";status("Loading recipients...");return;}if(payload.errors&&payload.errors.length){setColumnWidths([]);showErr(payload.errors.join("; "));headRow.innerHTML="<th>Unable to render recipients.</th>";bodyEl.innerHTML="";status("Load failed.");rebuildSortColumns([], "");return;}var rows=(payload.rows||[]).map(function(r,i){var out=(r&&typeof r==="object")?Object.assign({},r):{};out.__rr_index=i;return out;});var cols=normalizeColumns(payload.columns||[],rows);cols=dedupeEquivalentColumns(cols,rows);if(cols.length===0){setColumnWidths([]);headRow.innerHTML="<th>No fields</th>";bodyEl.innerHTML="";status("Rows: 0 | Total from query: "+(payload.count||0)+" | Selected: 0");rebuildSortColumns([], "");return;}if(state.sortBy&&cols.indexOf(state.sortBy)===-1){state.sortBy="";}rebuildSortColumns(cols,state.sortBy);var filtered=rows.filter(function(r){return rowMatches(r||{},state.filter,cols);});if(state.sortBy){filtered.sort(function(a,b){var l=text((a||{})[state.sortBy]).toLowerCase();var r=text((b||{})[state.sortBy]).toLowerCase();var cmp=l<r?-1:(l>r?1:0);return state.sortDirection==="desc"?-cmp:cmp;});}';
|
||||
echo 'setColumnWidths(cols);headRow.innerHTML="";var selHead=document.createElement("th");selHead.className="feca-rr-head-cell";selHead.innerHTML="<input id=\\"rr_select_all\\" type=\\"checkbox\\" title=\\"Select/Deselect all visible rows\\">";headRow.appendChild(selHead);cols.forEach(function(c){var th=document.createElement("th");th.className="feca-rr-head-cell";var p=headerParts(c);if(p.prefix){th.innerHTML="<div class=\\"feca-rr-head-wrap\\"><span class=\\"feca-rr-head-prefix\\">"+p.prefix+"</span><span class=\\"feca-rr-head-field\\">"+p.field+"</span></div>";}else{th.innerHTML="<div class=\\"feca-rr-head-wrap\\"><span class=\\"feca-rr-head-field\\">"+p.field+"</span></div>";}headRow.appendChild(th);});bodyEl.innerHTML="";filtered.forEach(function(r){var tr=document.createElement("tr");var rowKey=detectKey(r);tr.setAttribute("data-row-key",rowKey);var selTd=document.createElement("td");selTd.className="feca-rr-data-cell";var checked=!!(rowKey&&state.selectedKeys[rowKey]);selTd.innerHTML="<input class=\\"rr-row-select\\" type=\\"checkbox\\" data-key=\\""+String(rowKey).replace(/"/g,""")+"\\""+(checked?" checked":"")+">";tr.appendChild(selTd);cols.forEach(function(c){var td=document.createElement("td");var v=text((r||{})[c]);td.className="feca-rr-data-cell";td.title=v;td.textContent=v;tr.appendChild(td);});bodyEl.appendChild(tr);});';
|
||||
echo 'var allKeys=rows.map(detectKey).filter(function(k){return !!k;});var selectedInSource=0;allKeys.forEach(function(k){if(state.selectedKeys[k]){selectedInSource++;}});var visibleKeys=filtered.map(detectKey).filter(function(k){return !!k;});var allVisibleSelected=visibleKeys.length>0&&visibleKeys.every(function(k){return !!state.selectedKeys[k];});var selAll=document.getElementById("rr_select_all");if(selAll){selAll.checked=allVisibleSelected;selAll.indeterminate=visibleKeys.length>0&&!allVisibleSelected&&visibleKeys.some(function(k){return !!state.selectedKeys[k];});}status("Rows: "+filtered.length+" | Total from query: "+(payload.count||0)+" | Selected: "+selectedInSource+(state.filter?(" | Filter: "+state.filter):""));saveSelection();}';
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ use PDO;
|
|||
|
||||
final class DataSourceService
|
||||
{
|
||||
private const NAME_MAX_LENGTH = 100;
|
||||
|
||||
private MailshotQueryRepository $queries;
|
||||
private DatabaseRouter $router;
|
||||
private DslParser $parser;
|
||||
|
|
@ -111,22 +109,6 @@ final class DataSourceService
|
|||
$this->queries->delete($id);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function duplicate(int $id): array
|
||||
{
|
||||
$source = $this->queries->find($id);
|
||||
if ($source === null) {
|
||||
return ['ok' => false, 'errors' => ['Data source not found.']];
|
||||
}
|
||||
|
||||
$newId = $this->queries->create([
|
||||
'name' => $this->duplicateName((string) ($source['name'] ?? ''), $id),
|
||||
'dsl_text' => (string) ($source['dsl_text'] ?? ''),
|
||||
]);
|
||||
|
||||
return ['ok' => true, 'id' => $newId];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function preview(string $dsl, int $limit = 50): array
|
||||
{
|
||||
|
|
@ -249,41 +231,4 @@ final class DataSourceService
|
|||
}
|
||||
return $this->metadata->sourceFields($source);
|
||||
}
|
||||
|
||||
private function duplicateName(string $name, int $sourceId): string
|
||||
{
|
||||
$base = trim($name);
|
||||
if ($base === '') {
|
||||
$base = 'Data source #' . $sourceId;
|
||||
}
|
||||
|
||||
$existing = [];
|
||||
foreach ($this->queries->all() as $row) {
|
||||
$existing[strtolower(trim((string) ($row['name'] ?? '')))] = true;
|
||||
}
|
||||
|
||||
$prefix = 'Copy of ';
|
||||
$candidate = $this->truncateName($prefix . $base);
|
||||
if (!isset($existing[strtolower($candidate)])) {
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
for ($i = 2; $i < 1000; $i++) {
|
||||
$suffix = ' (' . $i . ')';
|
||||
$candidate = $this->truncateName($prefix . $base, $suffix);
|
||||
if (!isset($existing[strtolower($candidate)])) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->truncateName($prefix . $base, ' (' . time() . ')');
|
||||
}
|
||||
|
||||
private function truncateName(string $value, string $suffix = ''): string
|
||||
{
|
||||
$suffixLength = strlen($suffix);
|
||||
$baseMax = max(1, self::NAME_MAX_LENGTH - $suffixLength);
|
||||
$base = substr($value, 0, $baseMax);
|
||||
return rtrim($base) . $suffix;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,8 +305,8 @@ final class MailshotRunService
|
|||
$selectedSet = array_fill_keys($selectedRecipientKeys, true);
|
||||
$filteredRows = [];
|
||||
foreach (array_values($rows) as $index => $row) {
|
||||
$rowIndexKey = 'row_index:' . $index;
|
||||
if (isset($selectedSet[$rowIndexKey])) {
|
||||
[, $recipientKey] = $this->detectRecipientKey((array) $row, $index);
|
||||
if (isset($selectedSet[$recipientKey])) {
|
||||
$filteredRows[] = $row;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,4 +23,3 @@
|
|||
* [ ] I made an edit to a mailshot (), with a 9kb message. Save did not close the dialog - but did not show any errors. There was a long pause (long enough to start typing this report) before it appeared to have had an effect (mailshot saved message). But Edit button on Mailshot doesn't appear to do anything now browser shows page still loading. https://fenedge.co.uk is not responding.
|
||||
* [ ] Edit Data Source save DSL="ads and advertisers where issue(125)" takes a very long long time to repond.
|
||||
* [X] Downloaded merged pdf or .zip should be named after maiilshot purpose
|
||||
* [X] Mailshot data sources. Add "Duplicate" button per row.
|
||||
|
|
|
|||
|
|
@ -38,41 +38,6 @@ test('data sources: create + preview + validation failure path', async ({ page,
|
|||
}
|
||||
});
|
||||
|
||||
test('data sources: duplicate row action creates editable copy', async ({ page, request }) => {
|
||||
const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
const dsName = `e2e_ds_duplicate_${uniq}`;
|
||||
const copyName = `Copy of ${dsName}`;
|
||||
|
||||
try {
|
||||
await ensureDataSource(request, dsName, 'contacts and accounts');
|
||||
|
||||
await page.goto(adminPath('feca-mailshot-data-sources'));
|
||||
await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible();
|
||||
|
||||
const row = page.locator('tr', {
|
||||
has: page.getByRole('cell', { name: dsName })
|
||||
}).first();
|
||||
await expect(row).toBeVisible();
|
||||
await row.getByRole('button', { name: 'Duplicate' }).click();
|
||||
|
||||
const copyRow = page.locator('tr', {
|
||||
has: page.getByRole('cell', { name: copyName })
|
||||
}).first();
|
||||
await expect(copyRow).toBeVisible();
|
||||
await copyRow.getByRole('link', { name: 'Edit' }).click();
|
||||
|
||||
await expect(page.locator('#ds-editor-modal')).toBeVisible();
|
||||
await expect(page.locator('#ds_name')).toHaveValue(copyName);
|
||||
await expect(page.locator('#ds_dsl')).toHaveValue('contacts and accounts');
|
||||
} finally {
|
||||
try {
|
||||
await cleanupByNames(request, { dataSourceNames: [dsName, copyName] });
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('data sources: DSL builder round-trip preserves complex representable DSL', async ({ page, request }) => {
|
||||
const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`;
|
||||
const dsName = `e2e_ds_roundtrip_${uniq}`;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ $dataSourcesHtml = $capture(static function () use ($container): void {
|
|||
});
|
||||
$assertContains('New Data Source', $dataSourcesHtml, 'Data Sources');
|
||||
$assertContains('feca_mailshots_data_sources_ui_save', $dataSourcesHtml, 'Data Sources');
|
||||
$assertContains('feca_mailshots_data_sources_ui_duplicate', $dataSourcesHtml, 'Data Sources');
|
||||
$assertContains('Existing Data Sources', $dataSourcesHtml, 'Data Sources');
|
||||
$assertNotContains('Use endpoint', $dataSourcesHtml, 'Data Sources');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue