Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ba15359
Update gpl license from gpl-2 to gpl-3 #39
Ankur0812 Nov 14, 2024
ed1d72f
Added blueprint.json #40
Ankur0812 Nov 14, 2024
eacba36
Update assets/blueprints/blueprint.json
mehul0810 May 21, 2025
9fedb24
Merge pull request #43 from performwp/add-blueprint.json-40
mehul0810 May 21, 2025
1bca67c
Merge pull request #42 from performwp/fix-issue-39
mehul0810 May 21, 2025
ec9c67a
Merge branch 'master' into develop
Jun 25, 2025
40dc5ad
change git repo to org repo
Jun 25, 2025
d648a67
updated package lock json
Jun 25, 2025
e6227d0
add settings app using react
Jul 8, 2025
1c9cb06
added settings header
Jul 8, 2025
b39cf87
added settings navigation
Jul 8, 2025
188b152
loading of section and fields
Jul 8, 2025
d749b53
show fields with relevant components
Jul 8, 2025
0b2e42e
added support for some more settings
Jul 9, 2025
52c6fa8
implemented new settings ui
Oct 28, 2025
7eb16fa
remove legacy settings section
Oct 28, 2025
4fb977b
improve ui for footer section
Oct 28, 2025
14ac81b
updated setting fields
Oct 29, 2025
6908623
added support for textarea control
Oct 29, 2025
d2ef20b
added support for help link
Oct 29, 2025
b4e1e20
improve the card layout ui
Oct 29, 2025
784c13c
added capability check for save settings
Oct 29, 2025
90da8dd
improved clean helper fn
Oct 29, 2025
d78f942
improved field specific sanitization
Oct 29, 2025
9b264c6
update version to 1.5.0
Oct 29, 2025
c99c61a
update language pot file
Oct 29, 2025
265ba6d
fix menu cache migration issue
Oct 30, 2025
db8adad
fix ssl migration issue
Ankur0812 Oct 30, 2025
945a28f
add scripts to composer
Nov 3, 2025
c82014d
removed unused variable
mehul0810 Nov 3, 2025
5510a93
remove unnecessary inline comment
mehul0810 Nov 3, 2025
49d67dc
minor fixes for automation with gh
Nov 3, 2025
1afd38b
minor fix for lint automation
Nov 3, 2025
87ae9cd
some more fixes to yml files
Nov 3, 2025
b145b66
update composer json
Nov 3, 2025
a966760
minor improvements
Nov 3, 2025
90c6fd6
relocate assets for wp org plugin
Nov 3, 2025
1332af0
update banner images
Nov 3, 2025
55daa79
update readme txt file
Nov 3, 2025
115b3ba
added some cta
Nov 3, 2025
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
19 changes: 14 additions & 5 deletions .github/workflows/code-style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,26 @@ jobs:
with:
php-version: '8.2'
coverage: none
tools: cs2pr

# Validate the composer.json file.
# @link https://getcomposer.org/doc/03-cli.md#validate
- name: Validate Composer installation
run: composer validate --no-check-all
run: |
# Don't fail the workflow on composer validate warnings (lock file mismatch is common
# after editing composer.json in a branch). Print guidance so maintainers can fix the lock.
composer validate --no-check-all || true
echo "Note: composer validate returned warnings. If the lock file is out of date, run locally:"
echo " composer update --lock"
echo "or to update a specific package: composer update staabm/cs2pr --with-dependencies"
echo "Then commit the updated composer.lock to the branch."

# Install dependencies and handle caching in one go.
# @link https://github.com/marketplace/actions/install-composer-dependencies
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
with:
# Bust the cache at least once a month - output format: YYYY-MM.
custom-cache-suffix: $(date -u "+%Y-%m")
# Use a reproducible, unique suffix from the run id to avoid shell evaluation problems.
custom-cache-suffix: ${{ github.run_id }}

# Check the codestyle of the files.
# The results of the CS check will be shown inline in the PR via the CS2PR tool.
Expand All @@ -65,4 +71,7 @@ jobs:

- name: Show PHPCS results in PR
if: ${{ always() && steps.phpcs.outcome == 'failure' }}
run: cs2pr ./phpcs-report.xml
uses: staabm/annotate-pull-request-from-checkstyle@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
checkstyle_report: ./phpcs-report.xml
14 changes: 10 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,25 @@ jobs:
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
with:
# Bust the cache at least once a month - output format: YYYY-MM-DD.
custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
# Use run id so no shell-specific date expansion is required here.
custom-cache-suffix: ${{ github.run_id }}

- name: Install PHP for the actual test
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php_version }}
ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
coverage: none
tools: cs2pr

- name: Lint against parse errors
run: composer lint -- --checkstyle | cs2pr
run: composer lint -- --checkstyle > ./lint-report.xml || true

- name: Show lint results in PR
if: ${{ always() }}
uses: staabm/annotate-pull-request-from-checkstyle@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
checkstyle_report: ./lint-report.xml

- name: Lint blueprint file
run: composer lint-blueprint
2 changes: 1 addition & 1 deletion .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
with:
php-version: 'latest'
coverage: none
tools: composer, cs2pr
tools: composer

- name: Install PHP dependencies
uses: ramsey/composer-install@v2
Expand Down
Binary file added .wordpress-org/banner-1544x500-rtl.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/banner-1544x500.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/banner-772x250-rtl.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/banner-772x250.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions .wordpress-org/blueprints/blueprint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"landingPage": "/wp-admin/options-general.php?page=perform",
"features": {
"networking": true
},
"plugins": [
"perform"
],
"preferredVersions": {
"php": "8.2",
"wp": "6.8"
},
"steps": [
{
"step": "login"
}
]
}
18 changes: 18 additions & 0 deletions .wordpress-org/blueprints/playground.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"landingPage": "/wp-admin/options-general.php?page=perform",
"features": {
"networking": true
},
"plugins": [
"perform"
],
"preferredVersions": {
"php": "8.2",
"wp": "6.8"
},
"steps": [
{
"step": "login"
}
]
}
Binary file added .wordpress-org/icon-128x128.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/icon-256x256.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/screenshot-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/screenshot-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/screenshot-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/screenshot-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion assets/src/css/_mixins.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
--menu-color: #f0f0f1;
--menu-hover-color: #f5f5f5;
--text-color: #333333;
}
--white-color: #ffffff;
}
54 changes: 54 additions & 0 deletions assets/src/css/admin/settings.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
.perform-settings-page {
margin-left: -20px;
}

.perform-settings-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
background-color: var(--white-color);
padding: 0px 20px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

.perform-plugin-version {
font-size: 10px;
color: var(--white-color);
background-color: var(--primary-color);
padding: 3px 8px;
border-radius: 5px;
}

.perform-settings-tab-panel {
background-color: var(--bg-color);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}


.perform-card-title,
.perform-card-description {
margin: 0;
}

.perform-settings-content {
padding: 0 20px;
}


.perform-admin-settings-wrap {
display: flex;
}
Expand All @@ -14,3 +53,18 @@
text-decoration: none;
color: var(--text-color);
}

/* Help link styling for settings fields */
.perform-help-link {
text-decoration: none; /* remove underline */
color: var(--primary-color);
font-weight: 500;
}
.perform-help-link:hover,
.perform-help-link:focus {
text-decoration: underline; /* show underline on hover for affordance */
color: var(--primary-color) !important;
}
.perform-help-icon {
margin-left: 0;
}
13 changes: 13 additions & 0 deletions assets/src/js/admin/Card.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Card, CardHeader, CardBody } from '@wordpress/components';

const SettingsCard = ({ title, description, children }) => (
<Card style={{ marginBottom: '24px' }}>
<CardHeader>
<strong>{title}</strong>
</CardHeader>
{description && <CardBody><p>{description}</p></CardBody>}
<CardBody>{children}</CardBody>
</Card>
);

export default SettingsCard;
35 changes: 35 additions & 0 deletions assets/src/js/admin/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Button, Spinner } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

Unused import useEffect.

Suggested change
import { useEffect } from '@wordpress/element';

Copilot uses AI. Check for mistakes.

const Footer = ({ dirty, saving, message, onSave }) => {
// message: { text, type } where type is 'success' | 'error' | ''
return (
<div className="perform-savebar" style={{
position: 'sticky',
bottom: 0,
left: 0,
right: 0,
background: '#fff',
borderTop: '1px solid #eee',
padding: 12,
zIndex: 1000,
display: 'flex',
justifyContent: 'flex-end',
gap: 12,
alignItems: 'center',
marginBottom: '-20px',
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

[nitpick] Using a negative margin to adjust positioning is a code smell that suggests the layout structure may need adjustment. Consider reviewing the parent container's padding/margin instead of using negative values, which can cause unexpected layout issues and make the component less reusable.

Suggested change
marginBottom: '-20px',

Copilot uses AI. Check for mistakes.
}}>
<div style={{ marginRight: 'auto' }} />
{message && message.text && (
<div style={{ marginRight: 12, color: message.type === 'error' ? '#c00' : '#146e00' }}>
{message.text}
</div>
)}
<Button isPrimary onClick={onSave} disabled={!dirty || saving}>
{saving ? <><Spinner /> Saving...</> : 'Save Settings'}
</Button>
</div>
);
};

export default Footer;
140 changes: 140 additions & 0 deletions assets/src/js/admin/SettingsApp.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import SettingsHeader from './SettingsHeader';
import SettingsNav from './SettingsNav';
import Footer from './Footer';
import { useState, useEffect, useMemo, useRef } from '@wordpress/element';

const SettingsApp = () => {
const tabs = window.performwpSettings?.tabs || {};
const fields = window.performwpSettings?.fields || {};
const initialValues = useMemo(() => {
// Build a map of field id => saved value (if present) or default value (empty string or false)
const saved = window.performwpSettings?.saved || {};
const values = {};
Object.keys(fields).forEach((tab) => {
fields[tab].forEach((card) => {
(card.fields || []).forEach((f) => {
const savedVal = saved && Object.prototype.hasOwnProperty.call(saved, f.id) ? saved[f.id] : undefined;
if (typeof savedVal !== 'undefined') {
values[f.id] = savedVal;
} else {
values[f.id] = f.default ?? (f.type === 'toggle' ? false : '');
}
});
});
});
return values;
}, [fields]);

const [fieldValues, setFieldValues] = useState(initialValues);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState(null);
const [activeTab, setActiveTab] = useState(Object.keys(tabs)[0] || '');
const messageTimerRef = useRef(null);

// dirty detection

const handleFieldChange = (id, value) => {
setFieldValues((prev) => ({ ...prev, [id]: value }));
};

const handleSave = async () => {
setSaving(true);
setMessage(null);
try {
const res = await fetch(ajaxurl, {
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

The code uses the global 'ajaxurl' variable without checking if it exists. While this is typically available in WordPress admin, it's better to access it safely via window.ajaxurl or provide a fallback. Consider adding a check or using window.performwpSettings to pass the ajax URL explicitly.

Suggested change
const res = await fetch(ajaxurl, {
const ajaxUrl = window.performwpSettings?.ajaxurl || window.ajaxurl;
if (!ajaxUrl) {
throw new Error('AJAX URL not defined.');
}
const res = await fetch(ajaxUrl, {

Copilot uses AI. Check for mistakes.
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: new URLSearchParams({
action: 'perform_save_settings',
nonce: window.performwpSettings?.nonce || '',
data: JSON.stringify(fieldValues)
})
});
const json = await res.json();
if ( json && json.success ) {
setMessage({ text: json.data?.message || 'Settings saved.', type: 'success' });
// update initialValues snapshot
// mutate initialValues object won't update memo, so reset by rebuild: setFieldValues equals current, but we need to reset initialValues - simplest approach: set initial snapshot to current by resetting via a state.
// We'll set the initialValues by replacing the state used for comparison: emulate by setting all initialValues to current values via a ref - but here we'll just clear dirty by resetting initialValues via resetting fieldValues baseline.
// For simplicity, update initialValues by assigning to window.performwpSettings._initial = fieldValues (not ideal), but we can update local initialValues via a small trick: setFieldValues to same and update a savedSnapshot state.
// Implement savedSnapshot state instead.
Comment on lines +59 to +62
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

These commented-out thoughts/implementation notes should be removed. They describe the problem-solving process but don't add value to the final code. The implementation using savedSnapshot is clear from the code itself. Remove these comments to improve code cleanliness.

Suggested change
// mutate initialValues object won't update memo, so reset by rebuild: setFieldValues equals current, but we need to reset initialValues - simplest approach: set initial snapshot to current by resetting via a state.
// We'll set the initialValues by replacing the state used for comparison: emulate by setting all initialValues to current values via a ref - but here we'll just clear dirty by resetting initialValues via resetting fieldValues baseline.
// For simplicity, update initialValues by assigning to window.performwpSettings._initial = fieldValues (not ideal), but we can update local initialValues via a small trick: setFieldValues to same and update a savedSnapshot state.
// Implement savedSnapshot state instead.

Copilot uses AI. Check for mistakes.
} else {
setMessage({ text: (json && json.data && json.data.message) || 'Save failed.', type: 'error' });
}
} catch (e) {
setMessage({ text: e.message || 'Save failed.', type: 'error' });
} finally {
setSaving(false);
}
};

// Add a savedSnapshot state to serve as baseline for dirty calculation
const [savedSnapshot, setSavedSnapshot] = useState(initialValues);

useEffect(() => {
// when initialValues changes (first render) set snapshot
setSavedSnapshot(initialValues);
setFieldValues(initialValues);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValues]);

// recompute dirty based on savedSnapshot
const isDirty = useMemo(() => {
return Object.keys(fieldValues).some((k) => fieldValues[k] !== savedSnapshot[k]);
}, [fieldValues, savedSnapshot]);

// update savedSnapshot on successful save by watching message success
useEffect(() => {
if (message && message.type === 'success') {
setSavedSnapshot(fieldValues);
}
}, [message, fieldValues]);

// Auto-dismiss message after 5 seconds
useEffect(() => {
if (!message || !message.text) return;
// Clear previous timer
if (messageTimerRef.current) {
clearTimeout(messageTimerRef.current);
messageTimerRef.current = null;
}
messageTimerRef.current = setTimeout(() => {
setMessage(null);
messageTimerRef.current = null;
}, 5000);

return () => {
if (messageTimerRef.current) {
clearTimeout(messageTimerRef.current);
messageTimerRef.current = null;
}
};
}, [message]);

// Clear timer on unmount
useEffect(() => () => {
if (messageTimerRef.current) {
clearTimeout(messageTimerRef.current);
messageTimerRef.current = null;
}
}, []);

return (
<>
<SettingsHeader />
<SettingsNav
fields={fields}
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
fieldValues={fieldValues}
onFieldChange={handleFieldChange}
/>
<Footer dirty={isDirty} saving={saving} message={message} onSave={handleSave} />
</>
);
};

export default SettingsApp;
Loading
Loading