Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 337 additions & 0 deletions assets/js/dns-management.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
/* global jQuery, ajaxurl, wu_dns_config */
/**
* DNS Management Vue.js Component
*
* Handles DNS record display and management in the Ultimate Multisite UI.
*
* @since 2.3.0
*/
(function($) {
'use strict';

/**
* Initialize DNS Management when DOM is ready.
*/
$(document).ready(function() {
initDNSManagement();
});

/**
* Initialize the DNS Management Vue instance.
*/
function initDNSManagement() {
const container = document.getElementById('wu-dns-records-table');

if (!container || typeof Vue === 'undefined') {
return;
}

// Check if Vue instance already exists
if (container.__vue__) {
return;
}

window.WU_DNS_Management = new Vue({
el: '#wu-dns-records-table',
data: {
loading: true,
error: null,
records: [],
readonly: false,
domain: '',
domainId: '',
canManage: false,
provider: '',
recordTypes: ['A', 'AAAA', 'CNAME', 'MX', 'TXT'],
selectedRecords: [],
},

computed: {
hasRecords: function() {
return this.records && this.records.length > 0;
},

sortedRecords: function() {
if (!this.records) {
return [];
}

// Sort by type, then by name
return [...this.records].sort(function(a, b) {
if (a.type !== b.type) {
return a.type.localeCompare(b.type);
}
return a.name.localeCompare(b.name);
});
},
},

mounted: function() {
const el = this.$el;

this.domain = el.dataset.domain || '';
this.domainId = el.dataset.domainId || '';
this.canManage = el.dataset.canManage === 'true';

if (this.domain) {
this.loadRecords();
}
},

methods: {
/**
* Load DNS records from the server.
*/
loadRecords: function() {
const self = this;

this.loading = true;
this.error = null;

$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'wu_get_dns_records_for_domain',
nonce: wu_dns_config.nonce,
domain: this.domain,
},
success: function(response) {
self.loading = false;

if (response.success) {
self.records = response.data.records || [];
self.readonly = response.data.readonly || false;
self.provider = response.data.provider || '';

if (response.data.record_types) {
self.recordTypes = response.data.record_types;
}

if (response.data.message && self.readonly) {
self.error = response.data.message;
}
} else {
self.error = response.data?.message || 'Failed to load DNS records.';
}
Comment on lines +114 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

WordPress JavaScript optional chaining ES2020 support transpilation requirements

💡 Result:

Short answer

  • Optional chaining (?.) is an ES2020 feature. If you use WordPress’s official build tools (@wordpress/build, @wordpress/babel-preset-default or the Gutenberg browserslist config) your code will be transpiled so you normally do NOT need to add a separate optional‑chaining plugin. [1][2][3].
  • If you use a custom toolchain you must ensure the parser/transpiler runs before a tool that cannot parse ?. (common pitfalls: webpack4’s parser). Options: use @wordpress/build or @wordpress/babel-preset-default, add @babel/plugin-transform-optional-chaining (or the plugin-proposal), compile with esbuild/TypeScript target ≤ ES2019, or upgrade to a parser that understands ES2020 (e.g. Webpack 5). [4][5][6]

Key references

  1. @wordpress/wp-build (uses esbuild and produces transpiled output). [1]
  2. @wordpress/babel-preset-default (WordPress’s Babel preset; extendable). [3]
  3. @wordpress/browserslist-config (controls target browsers that determine what gets transpiled). [2]
  4. Babel docs: transform/syntax/optional‑chaining and that preset‑env includes the transform for ES2020. [5]
  5. Webpack/Babel discussions: webpack4 parser may choke on untransformed ?. — either transpile earlier or upgrade to Webpack 5. [6][4]

If you want, tell me which build setup you use (WordPress tools, create-react-app, webpack config, TypeScript, @wordpress/scripts) and I’ll give exact config lines to add.


🏁 Script executed:

# Look for build configuration files
fd -type f -name "webpack.config.*" -o -name "babel.config.*" -o -name ".babelrc*" -o -name "package.json" | head -20

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 251


🏁 Script executed:

# Check the directory structure to understand the project layout
git ls-files | head -30

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 908


🏁 Script executed:

# Find build configuration files
find . -maxdepth 3 -type f \( -name "webpack.config.js" -o -name "webpack.config.json" -o -name "babel.config.js" -o -name ".babelrc" -o -name "package.json" \) 2>/dev/null | grep -v node_modules | head -20

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 95


🏁 Script executed:

# Check if dns-management.js is in the assets directory and look at the project structure
find . -name "dns-management.js" -o -name "package.json" | head -10

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 125


🏁 Script executed:

# Read package.json to check build dependencies and configuration
cat -n package.json

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 4082


🏁 Script executed:

# Check the uglify build script to understand how JavaScript is processed
cat -n scripts/uglify.js | head -50

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 644


🏁 Script executed:

# Check if there are any other JavaScript preprocessing steps
cat -n scripts/clean-js.js | head -30

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 180


🏁 Script executed:

# Check if there's any browserslist or browser support documentation
find . -maxdepth 2 -type f \( -name ".browserslistrc" -o -name "browserslist" \) 2>/dev/null

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 63


🏁 Script executed:

# Check the plugin header or readme for browser requirements
head -50 ultimate-multisite.php | grep -i "requires\|browser\|support"

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 323


🏁 Script executed:

# Check README for browser/support information
cat -n README.md 2>/dev/null | head -100

Repository: Multisite-Ultimate/ultimate-multisite

Length of output: 6204


Replace optional chaining with explicit null checks or add Babel transpilation.

The build process uses UglifyJS for minification only, which does not transpile ES2020 syntax like optional chaining (?.). This code will fail in older browsers (IE11, Safari <14, etc.) that don't support optional chaining. Either add @babel/plugin-transform-optional-chaining to your Babel configuration, or replace response.data?.message with response.data && response.data.message.

Also applies to: 296-298

🤖 Prompt for AI Agents
In assets/js/dns-management.js around lines 114 to 116 (and also apply the same
fix at 296-298), the use of optional chaining (response.data?.message) is not
compatible with the current build/minifier; replace the optional chaining with
an explicit null/undefined check such as using response.data &&
response.data.message (or response.data ? response.data.message : undefined), or
alternatively add @babel/plugin-transform-optional-chaining to the project's
Babel config so optional chaining is transpiled during the build.

},
error: function(xhr, status, errorMsg) {
self.loading = false;
self.error = 'Network error: ' + errorMsg;
},
});
},

/**
* Refresh the records list.
*/
refresh: function() {
this.loadRecords();
},

/**
* Get CSS class for record type badge.
*
* @param {string} type The record type.
* @return {string} CSS classes.
*/
getTypeClass: function(type) {
const classes = {
'A': 'wu-bg-blue-100 wu-text-blue-800',
'AAAA': 'wu-bg-purple-100 wu-text-purple-800',
'CNAME': 'wu-bg-green-100 wu-text-green-800',
'MX': 'wu-bg-orange-100 wu-text-orange-800',
'TXT': 'wu-bg-gray-100 wu-text-gray-800',
};

return classes[type] || 'wu-bg-gray-100 wu-text-gray-800';
},

/**
* Format TTL value for display.
*
* @param {number} seconds TTL in seconds.
* @return {string} Formatted TTL.
*/
formatTTL: function(seconds) {
if (seconds === 1) {
return 'Auto';
}

if (seconds < 60) {
return seconds + 's';
}

if (seconds < 3600) {
return Math.floor(seconds / 60) + 'm';
}

if (seconds < 86400) {
return Math.floor(seconds / 3600) + 'h';
}

return Math.floor(seconds / 86400) + 'd';
},

/**
* Truncate content for display.
*
* @param {string} content The content to truncate.
* @param {number} maxLength Maximum length.
* @return {string} Truncated content.
*/
truncateContent: function(content, maxLength) {
maxLength = maxLength || 40;

if (!content || content.length <= maxLength) {
return content;
}

return content.substring(0, maxLength) + '...';
},

/**
* Get the edit URL for a record.
*
* @param {Object} record The record object.
* @return {string} Edit URL.
*/
getEditUrl: function(record) {
if (!wu_dns_config.edit_url) {
return '#';
}

return wu_dns_config.edit_url +
'&record_id=' + encodeURIComponent(record.id) +
'&domain_id=' + encodeURIComponent(this.domainId);
},

/**
* Get the delete URL for a record.
*
* @param {Object} record The record object.
* @return {string} Delete URL.
*/
getDeleteUrl: function(record) {
if (!wu_dns_config.delete_url) {
return '#';
}

return wu_dns_config.delete_url +
'&record_id=' + encodeURIComponent(record.id) +
'&domain_id=' + encodeURIComponent(this.domainId);
},

/**
* Toggle record selection.
*
* @param {string} recordId The record ID.
*/
toggleSelection: function(recordId) {
const index = this.selectedRecords.indexOf(recordId);

if (index > -1) {
this.selectedRecords.splice(index, 1);
} else {
this.selectedRecords.push(recordId);
}
},

/**
* Check if a record is selected.
*
* @param {string} recordId The record ID.
* @return {boolean} True if selected.
*/
isSelected: function(recordId) {
return this.selectedRecords.indexOf(recordId) > -1;
},

/**
* Select all records.
*/
selectAll: function() {
const self = this;

this.selectedRecords = this.records.map(function(record) {
return record.id;
});
},

/**
* Deselect all records.
*/
deselectAll: function() {
this.selectedRecords = [];
},

/**
* Delete selected records (admin bulk operation).
*/
deleteSelected: function() {
if (!this.selectedRecords.length) {
return;
}

if (!confirm('Are you sure you want to delete ' + this.selectedRecords.length + ' selected records?')) {
return;
}

const self = this;

$.ajax({
url: ajaxurl,
method: 'POST',
data: {
action: 'wu_bulk_dns_operations',
nonce: wu_dns_config.nonce,
domain: this.domain,
operation: 'delete',
records: this.selectedRecords,
},
success: function(response) {
if (response.success) {
self.selectedRecords = [];
self.loadRecords();
} else {
alert('Error: ' + (response.data?.message || 'Failed to delete records.'));
}
},
error: function() {
alert('Network error occurred.');
},
});
},

/**
* Get proxied status display.
*
* @param {Object} record The record object.
* @return {string} Proxied status HTML.
*/
getProxiedStatus: function(record) {
if (this.provider !== 'cloudflare') {
return '';
}

if (record.proxied) {
return '<span class="wu-text-orange-500" title="Proxied through Cloudflare">&#9729;</span>';
}

return '<span class="wu-text-gray-400" title="DNS only">&#9729;</span>';
},
},
});
}

/**
* Reinitialize DNS management when modal content is loaded.
* This handles wubox modal scenarios.
*/
$(document).on('wubox-load', function() {
setTimeout(function() {
initDNSManagement();
}, 100);
});

})(jQuery);
Loading
Loading