Skip to content

Commit 10c20d9

Browse files
stigiclaude
andauthored
fix(config-ui): group connections by name, not by list position (#9130)
The A-N and O-Z headings were produced by cutting the plugin list at the first plugin id starting with a letter from o to z. That agrees with the headings only while each config's `sort` value runs alphabetically, and it stopped doing so as plugins were appended in the order they were added: Asana, Kiro, Linear and incident.io are all listed after Opsgenie, so all four appeared under O-Z. Group by the first letter of the displayed name instead, which is what a reader is matching against, and extract the decision into a pure function with tests. Generated-by: Claude Code (Claude Opus 5) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 43b5728 commit 10c20d9

2 files changed

Lines changed: 81 additions & 12 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
import { describe, it, expect } from 'vitest';
20+
21+
import { splitPluginsByInitial } from '@/routes/connection/connections';
22+
23+
// The plugin list arrives ordered by each config's `sort` value, which is not
24+
// alphabetical: plugins have been appended in the order they were added.
25+
const byName: Record<string, string> = {
26+
argocd: 'ArgoCD',
27+
clickup: 'ClickUp',
28+
opsgenie: 'Opsgenie',
29+
pagerduty: 'PagerDuty',
30+
asana: 'Asana',
31+
linear: 'Linear',
32+
zentao: 'ZenTao',
33+
incidentio: 'incident.io',
34+
azuredevops: 'Azure DevOps',
35+
};
36+
const nameOf = (plugin: string) => byName[plugin] ?? plugin;
37+
38+
describe('splitPluginsByInitial', () => {
39+
it('groups by the displayed name, not by list position', () => {
40+
const [an, oz] = splitPluginsByInitial(
41+
['argocd', 'clickup', 'opsgenie', 'pagerduty', 'asana', 'linear', 'zentao'],
42+
nameOf,
43+
);
44+
// asana and linear follow opsgenie in the list, and used to be filed O-Z.
45+
expect(an).toEqual(['argocd', 'clickup', 'asana', 'linear']);
46+
expect(oz).toEqual(['opsgenie', 'pagerduty', 'zentao']);
47+
});
48+
49+
it('keeps every plugin, wherever it sits in the list', () => {
50+
const plugins = Object.keys(byName);
51+
const [an, oz] = splitPluginsByInitial(plugins, nameOf);
52+
expect([...an, ...oz].sort()).toEqual([...plugins].sort());
53+
});
54+
55+
it('compares case-insensitively, so a lowercase name still groups correctly', () => {
56+
const [an, oz] = splitPluginsByInitial(['incidentio', 'zentao'], nameOf);
57+
expect(an).toEqual(['incidentio']);
58+
expect(oz).toEqual(['zentao']);
59+
});
60+
61+
it('falls back to the plugin id when a config has no name', () => {
62+
const [an, oz] = splitPluginsByInitial(['unknown-plugin', 'another'], (p) => p);
63+
expect(an).toEqual(['another']);
64+
expect(oz).toEqual(['unknown-plugin']);
65+
});
66+
});

config-ui/src/routes/connection/connections.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ import * as S from './styled';
2929

3030
const SORT_START_WITH = ['o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
3131

32+
// Group by the displayed name's first letter. The headings used to be produced
33+
// by cutting the list at the first plugin id starting with o-z, which only
34+
// agrees with the headings while `sort` happens to run alphabetically — it
35+
// stopped doing so as plugins were appended in the order they were added, so
36+
// Asana, Kiro, Linear and incident.io all showed up under O-Z.
37+
export const splitPluginsByInitial = (plugins: string[], nameOf: (plugin: string) => string) => {
38+
const isOZ = (plugin: string) => SORT_START_WITH.includes((nameOf(plugin)[0] ?? '').toLowerCase());
39+
return [plugins.filter((plugin) => !isOZ(plugin)), plugins.filter(isOZ)];
40+
};
41+
3242
export const Connections = () => {
3343
const [type, setType] = useState<'list' | 'form'>();
3444
const [plugin, setPlugin] = useState('');
@@ -46,18 +56,11 @@ export const Connections = () => {
4656
const webhooks = useAppSelector(selectWebhooks);
4757

4858
const filterWebhookPlugins = plugins.filter((p) => p !== 'webhook');
49-
const index = filterWebhookPlugins.findIndex((p) => SORT_START_WITH.includes(p[0]));
50-
51-
const [firstPlugins, secondPlugins] = useMemo(() => {
52-
if (index > 0) {
53-
// Split into A-N / O-Z at the first O-Z plugin. Must be a two-way
54-
// slice — `chunk(list, index)` produces equal-size groups and the
55-
// destructure keeps only the first two, silently dropping any plugins
56-
// in the tail once the list exceeds 2*index.
57-
return [filterWebhookPlugins.slice(0, index), filterWebhookPlugins.slice(index)];
58-
}
59-
return [filterWebhookPlugins, []];
60-
}, [index]);
59+
60+
const [firstPlugins, secondPlugins] = useMemo(
61+
() => splitPluginsByInitial(filterWebhookPlugins, (plugin) => getPluginConfig(plugin)?.name ?? plugin),
62+
[filterWebhookPlugins],
63+
);
6164

6265
const handleShowListDialog = (plugin: string) => {
6366
setType('list');

0 commit comments

Comments
 (0)