Skip to content

Commit 6022a6d

Browse files
authored
Merge pull request #62 from codebtech/feat/max-flags-setup
Adds max flag setup
2 parents db59fac + 4c37284 commit 6022a6d

File tree

8 files changed

+157
-25
lines changed

8 files changed

+157
-25
lines changed

README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,26 @@ WordPress Feature flags plugin allow developers to configure features in plugins
88

99
## Hooks
1010

11-
### JS Filters
11+
### PHP filters
1212

13-
##### mrFeatureFlags.newFlag.defaultStatus
13+
#### `mr_feature_flags_max_allowed`
14+
15+
Filter to define the maximum number of allowed flags. It is recommended to keep this to default value, which is 20.
16+
17+
Example usage:
18+
19+
```php
20+
add_filter(
21+
'mr_feature_flags_max_allowed',
22+
static function () {
23+
return 10;
24+
}
25+
);
26+
```
27+
28+
### JS filters
29+
30+
##### `mrFeatureFlags.newFlag.defaultStatus`
1431

1532
The filter controls whether the new flag is enabled by default or not. Default `true`
1633

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ services:
1919
depends_on:
2020
- mysql
2121
mysql:
22-
image: mysql:8
22+
image: mysql:8.0
2323
env_file: .env
2424
environment:
2525
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}

includes/Api/Flags.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ class Flags {
2929
*/
3030
public static $option_name = 'mr_feature_flags';
3131

32+
/**
33+
* Maximum allowed flags.
34+
*
35+
* @var int $max_flags
36+
*/
37+
private static $max_flags = 20;
38+
3239
/**
3340
* Register feature flag endpoints.
3441
*
@@ -92,6 +99,16 @@ public function post_flags( WP_REST_Request $request ) {
9299
$input_data = $request->get_json_params();
93100

94101
if ( is_array( $input_data['flags'] ) ) {
102+
/**
103+
* Filter to update max allowed feature flags.
104+
*/
105+
$max_allowed_flags = apply_filters( 'mr_feature_flags_max_allowed', self::$max_flags );
106+
if ( count( $input_data['flags'] ) > $max_allowed_flags ) {
107+
// translators: %d is a placeholder for the maximum allowed flags.
108+
$error_message = sprintf( __( 'Cannot add more than %d flags', 'mr-feature-flags' ), $max_allowed_flags );
109+
return new WP_Error( 'flag_limit_exceeded', $error_message, array( 'status' => 400 ) );
110+
}
111+
95112
update_option( self::$option_name, $input_data['flags'] );
96113
return rest_ensure_response(
97114
array(
@@ -101,7 +118,7 @@ public function post_flags( WP_REST_Request $request ) {
101118
);
102119
}
103120

104-
return new WP_Error( 'invalid_input', 'Cannot update flags', array( 'status' => 400 ) );
121+
return new WP_Error( 'invalid_input', __( 'Cannot update flags', 'mr-feature-flags' ), array( 'status' => 400 ) );
105122
}
106123

107124
/**

playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default defineConfig({
1717
use: {
1818
baseURL: process.env.WP_BASE_URL,
1919
trace: 'on-first-retry',
20+
permissions: ['clipboard-read'],
2021
},
2122

2223
projects: [

src/components/Flags.tsx

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import SubmitControls from './SubmitControls';
66
import { getFlags, updateFlags } from '../utils';
77
import Header from './Header';
88
import { __ } from '@wordpress/i18n';
9-
import { dispatch } from '@wordpress/data';
9+
import { useDispatch } from '@wordpress/data';
1010

1111
const Layout = (): JSX.Element => {
1212
const [flags, setFlags] = useState<Flag[]>([]);
1313
const [isLoading, setIsLoading] = useState<boolean>(true);
1414
const [isSaving, setIsSaving] = useState<boolean>(false);
1515
const [disableSave, setDisableSave] = useState<boolean>(false);
1616

17+
const { createErrorNotice, createSuccessNotice } =
18+
useDispatch('core/notices');
19+
1720
useEffect(() => {
1821
const logFlags = async () => {
1922
const fetchedFlags = await getFlags();
@@ -26,15 +29,31 @@ const Layout = (): JSX.Element => {
2629
logFlags();
2730
}, [setFlags, setIsLoading]);
2831

29-
const remoteApi = useCallback(async (input: Flag[]) => {
30-
await updateFlags({ ...input });
31-
//@ts-ignore
32-
dispatch('core/notices').createSuccessNotice('Saved successfully!', {
33-
type: 'snackbar',
34-
id: 'mr-feature-flags-snackbar',
35-
icon: <></>,
36-
});
37-
}, []);
32+
const remoteApi = useCallback(
33+
async (input: Flag[]) => {
34+
try {
35+
const response = await updateFlags({ ...input });
36+
if ('status' in response && response.status === 200) {
37+
createSuccessNotice(
38+
__('Saved successfully!', 'mr-feature-flags'),
39+
{
40+
type: 'snackbar',
41+
id: 'mr-feature-flags-snackbar',
42+
icon: <></>,
43+
}
44+
);
45+
}
46+
} catch (error: unknown) {
47+
//@ts-ignore
48+
createErrorNotice(error?.message, {
49+
type: 'snackbar',
50+
id: 'mr-feature-flags-snackbar',
51+
icon: <></>,
52+
});
53+
}
54+
},
55+
[createErrorNotice, createSuccessNotice]
56+
);
3857

3958
const lastFlag = flags?.at(-1)?.id || 0;
4059

src/styles/settings.scss

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
bottom: 3.5rem;
1111
position: fixed;
1212

13+
div {
14+
float: right;
15+
right: 250px;
16+
}
17+
1318
.components-snackbar__icon {
14-
left: 18px;
1519
top: auto;
20+
left: 18px;
1621
}
1722
}
1823

@@ -55,7 +60,7 @@
5560
.mr-feature-flags-toggle {
5661
margin-top: 7px;
5762
margin-left: 40px;
58-
min-width: 150px;
63+
min-width: 130px !important;
5964
}
6065

6166
.mr-feature-flags-sdk {

tests/e2e/feature-flags.spec.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ERROR_FLAG_EXISTS, ERROR_FLAG_INVALID } from '../../src/constants';
55
test.use({ storageState: process.env.WP_AUTH_STORAGE });
66

77
test.describe('Feature flags', () => {
8-
test('Create and delete flags e2e scenarios', async ({ page, admin }) => {
8+
test.beforeEach(async ({ page, admin }) => {
99
await admin.visitAdminPage('/');
1010

1111
//Find the feature flags in side menu
@@ -15,18 +15,19 @@ test.describe('Feature flags', () => {
1515
await expect(
1616
page.getByRole('heading', { name: 'Feature Flags settings' })
1717
).toBeVisible();
18+
});
1819

20+
test('Create and delete flags e2e scenarios', async ({ page }) => {
1921
//Create new flag
2022
await page.getByRole('button', { name: 'Add Flag' }).click();
21-
// await expect(await page.getByRole('textbox').count()).toBe(4);
2223
await page.getByRole('textbox').last().fill('test');
2324
await page.getByRole('button', { name: 'Save' }).click();
2425
//Confirm save success
2526
expect(
2627
await page.getByLabel('Dismiss this notice').innerText()
2728
).toMatch(/Saved successfully!/);
2829

29-
//Toggle flag test
30+
//Toggle feature flag
3031
await page
3132
.locator('id=mr-feature-flag-item')
3233
.last()
@@ -45,23 +46,21 @@ test.describe('Feature flags', () => {
4546
//Create another flag with same name
4647
await page.getByRole('button', { name: 'Add Flag' }).click();
4748
await page.getByRole('textbox').last().fill('test');
48-
expect(await page.getByText(ERROR_FLAG_EXISTS)).toBeVisible();
49+
expect(page.getByText(ERROR_FLAG_EXISTS)).toBeVisible();
4950
expect(page.getByRole('button', { name: 'Save' })).toBeDisabled();
5051

5152
//update flag name to be unique and check text validation.
5253
await page.getByRole('textbox').last().fill('test 2');
53-
expect(await page.getByText(ERROR_FLAG_INVALID)).toBeVisible();
54-
54+
expect(page.getByText(ERROR_FLAG_INVALID)).toBeVisible();
5555
expect(page.getByRole('button', { name: 'Save' })).toBeDisabled();
5656

57+
//Delete the flag
5758
await page
5859
.locator('id=mr-feature-flag-item')
5960
.last()
6061
.getByLabel('Delete Flag')
6162
.click();
62-
6363
await page.getByRole('button', { name: 'Yes' }).click();
64-
6564
//Confirm delete success
6665
expect(
6766
await page.getByLabel('Dismiss this notice').innerText()
@@ -72,14 +71,31 @@ test.describe('Feature flags', () => {
7271
expect(
7372
page.getByRole('heading', { name: 'SDK for feature flag: test' })
7473
).toBeVisible();
74+
75+
// Check PHP Snippet clipboard details
76+
await page.getByLabel('Copy to clipboard').first().click();
77+
const phpClipboardText = await page.evaluate(
78+
'navigator.clipboard.readText()'
79+
);
80+
expect(phpClipboardText).toContain("Flag::is_enabled( 'test' )");
81+
82+
// Check JS Snippet clipboard details
83+
await page.getByLabel('Copy to clipboard').nth(1).click();
84+
const jsClipboardText: string = await page.evaluate(
85+
'navigator.clipboard.readText()'
86+
);
87+
expect(jsClipboardText).toContain(
88+
"window.mrFeatureFlags.isEnabled('test')"
89+
);
90+
//Close SDK modal
7591
await page.locator('button[aria-label="Close"]').click();
7692

93+
//Delete the created flag
7794
await page
7895
.locator('id=mr-feature-flag-item')
7996
.last()
8097
.getByLabel('Delete Flag')
8198
.click();
82-
8399
await page.getByRole('button', { name: 'Yes' }).click();
84100
});
85101
});

tests/integration/FlagsApiTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,63 @@ public function test_create_item_with_invalid_input_array() {
153153

154154
}
155155

156+
public function test_create_item_with_default_max_allowed_filter() {
157+
wp_set_current_user( self::$admin );
158+
$flags = [['id'=>1, 'name'=>'test', 'enabled'=>true],
159+
['id'=>2, 'name'=>'test2', 'enabled'=>false],
160+
['id'=>3, 'name'=>'test3', 'enabled'=>false],
161+
['id'=>4, 'name'=>'test4', 'enabled'=>false],
162+
['id'=>5, 'name'=>'test5', 'enabled'=>false],
163+
['id'=>6, 'name'=>'test6', 'enabled'=>false],
164+
['id' => 7, 'name' => 'test7', 'enabled' => true],
165+
['id' => 8, 'name' => 'test8', 'enabled' => false],
166+
['id' => 9, 'name' => 'test9', 'enabled' => false],
167+
['id' => 10, 'name' => 'test10', 'enabled' => false],
168+
['id' => 11, 'name' => 'test11', 'enabled' => false],
169+
['id' => 12, 'name' => 'test12', 'enabled' => false],
170+
['id' => 13, 'name' => 'test13', 'enabled' => false],
171+
['id' => 14, 'name' => 'test14', 'enabled' => false],
172+
['id' => 15, 'name' => 'test15', 'enabled' => false],
173+
['id' => 16, 'name' => 'test16', 'enabled' => false],
174+
['id' => 17, 'name' => 'test17', 'enabled' => false],
175+
['id' => 18, 'name' => 'test18', 'enabled' => false],
176+
['id' => 19, 'name' => 'test19', 'enabled' => false],
177+
['id' => 20, 'name' => 'test20', 'enabled' => false],
178+
['id' => 21, 'name' => 'test21', 'enabled' => false],];
179+
180+
$request = new WP_REST_Request( 'POST', self::$api_endpoint );
181+
$request->add_header( 'Content-Type', 'application/json' );
182+
$request->set_body( wp_json_encode( ['flags' => $flags] ) );
183+
$response = rest_get_server()->dispatch( $request );
184+
$response_message = $response->get_data()['message'];
185+
186+
$this->assertErrorResponse( 'flag_limit_exceeded', $response, 400 );
187+
$this->assertEquals('Maximum allowed flags are 20', $response_message);
188+
189+
}
190+
191+
public function test_create_item_with_custom_max_allowed_filter() {
192+
wp_set_current_user( self::$admin );
193+
194+
// Mock the filter hook
195+
$mocked_max_flags = 3;
196+
add_filter('mr_feature_flags_max_allowed', function () use ($mocked_max_flags) {
197+
return $mocked_max_flags;
198+
});
199+
200+
$flags = [['id'=>1, 'name'=>'test', 'enabled'=>true], ['id'=>2, 'name'=>'test2', 'enabled'=>false], ['id'=>3, 'name'=>'test2', 'enabled'=>false], ['id'=>4, 'name'=>'test2', 'enabled'=>false]];
201+
202+
$request = new WP_REST_Request( 'POST', self::$api_endpoint );
203+
$request->add_header( 'Content-Type', 'application/json' );
204+
$request->set_body( wp_json_encode( ['flags' => $flags] ) );
205+
$response = rest_get_server()->dispatch( $request );
206+
$response_message = $response->get_data()['message'];
207+
208+
$this->assertErrorResponse( 'flag_limit_exceeded', $response, 400 );
209+
$this->assertEquals('Maximum allowed flags are 3', $response_message);
210+
211+
}
212+
156213
public function test_create_item_without_input() {
157214
wp_set_current_user( self::$admin );
158215

0 commit comments

Comments
 (0)