Prevent duplicate server display during profile transitions
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.23.5',
|
macClient: '0.23.6',
|
||||||
gatewayClient: '0.24.5',
|
gatewayClient: '0.24.6',
|
||||||
gatewayBackend: '0.24.0',
|
gatewayBackend: '0.24.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -728,6 +728,7 @@ export function ClientOverviewPage({
|
|||||||
prompt={!profile.desiredServerId}
|
prompt={!profile.desiredServerId}
|
||||||
leaving={pickerState.leaving}
|
leaving={pickerState.leaving}
|
||||||
revealVersion={pickerState.revealVersion}
|
revealVersion={pickerState.revealVersion}
|
||||||
|
anchorServerId={pickerState.anchorServerId}
|
||||||
onSelect={(serverId) => selectServer(profile, serverId)}
|
onSelect={(serverId) => selectServer(profile, serverId)}
|
||||||
/>}
|
/>}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ interface ServerPickerProps {
|
|||||||
prompt: boolean;
|
prompt: boolean;
|
||||||
leaving: boolean;
|
leaving: boolean;
|
||||||
revealVersion: number;
|
revealVersion: number;
|
||||||
|
anchorServerId?: string;
|
||||||
onSelect: (id: string) => unknown;
|
onSelect: (id: string) => unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +180,7 @@ export function ServerPicker({
|
|||||||
prompt,
|
prompt,
|
||||||
leaving,
|
leaving,
|
||||||
revealVersion,
|
revealVersion,
|
||||||
|
anchorServerId = '',
|
||||||
onSelect,
|
onSelect,
|
||||||
}: ServerPickerProps) {
|
}: ServerPickerProps) {
|
||||||
const favoritesKey = `${FAVORITES_KEY}:${profileId}`;
|
const favoritesKey = `${FAVORITES_KEY}:${profileId}`;
|
||||||
@@ -207,7 +209,7 @@ export function ServerPicker({
|
|||||||
if (view === 'recent') return recent.flatMap((id) => found.find((server) => server.id === id) || []);
|
if (view === 'recent') return recent.flatMap((id) => found.find((server) => server.id === id) || []);
|
||||||
return found;
|
return found;
|
||||||
}, [servers, query, view, favorites, recent]);
|
}, [servers, query, view, favorites, recent]);
|
||||||
const results = filtered.filter(({ id }) => id !== selectedServerId);
|
const results = filtered.filter(({ id }) => id !== selectedServerId && id !== anchorServerId);
|
||||||
const pageCount = Math.max(1, Math.ceil(results.length / SERVER_RESULT_WINDOW));
|
const pageCount = Math.max(1, Math.ceil(results.length / SERVER_RESULT_WINDOW));
|
||||||
const visible = results.slice(page * SERVER_RESULT_WINDOW, (page + 1) * SERVER_RESULT_WINDOW);
|
const visible = results.slice(page * SERVER_RESULT_WINDOW, (page + 1) * SERVER_RESULT_WINDOW);
|
||||||
const grouped = servers.length >= 10;
|
const grouped = servers.length >= 10;
|
||||||
@@ -282,7 +284,7 @@ export function ServerPicker({
|
|||||||
<ServerCheckButton checking={checking} onClick={checkVisible} />
|
<ServerCheckButton checking={checking} onClick={checkVisible} />
|
||||||
</div>
|
</div>
|
||||||
<div className="client-server-grid">
|
<div className="client-server-grid">
|
||||||
<ServerRow
|
{servers[0].id !== anchorServerId && <ServerRow
|
||||||
server={servers[0]}
|
server={servers[0]}
|
||||||
selected={servers[0].id === selectedServerId}
|
selected={servers[0].id === selectedServerId}
|
||||||
favorite={false}
|
favorite={false}
|
||||||
@@ -290,7 +292,7 @@ export function ServerPicker({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
index={0}
|
index={0}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
/>
|
/>}
|
||||||
</div>
|
</div>
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
@@ -310,8 +312,8 @@ export function ServerPicker({
|
|||||||
));
|
));
|
||||||
|
|
||||||
const simpleServers = [
|
const simpleServers = [
|
||||||
...(selected ? [selected] : []),
|
...(selected && selected.id !== anchorServerId ? [selected] : []),
|
||||||
...servers.filter(({ id }) => id !== selectedServerId),
|
...servers.filter(({ id }) => id !== selectedServerId && id !== anchorServerId),
|
||||||
].slice(0, SIMPLE_SERVER_LIMIT);
|
].slice(0, SIMPLE_SERVER_LIMIT);
|
||||||
|
|
||||||
return <section className={`client-servers is-scalable${servers.length <= SIMPLE_SERVER_LIMIT ? ' is-short' : ''}`} aria-label="Выберите сервер">
|
return <section className={`client-servers is-scalable${servers.length <= SIMPLE_SERVER_LIMIT ? ' is-short' : ''}`} aria-label="Выберите сервер">
|
||||||
@@ -403,7 +405,7 @@ export function ServerPicker({
|
|||||||
fallback={autoActive ? undefined : 'Первый стабильный сервер'}
|
fallback={autoActive ? undefined : 'Первый стабильный сервер'}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{selected && <ServerRow
|
{selected && selected.id !== anchorServerId && <ServerRow
|
||||||
server={selected}
|
server={selected}
|
||||||
selected
|
selected
|
||||||
favorite={favorites.includes(selected.id)}
|
favorite={favorites.includes(selected.id)}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface ServerPickerRenderState {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
leaving: boolean;
|
leaving: boolean;
|
||||||
revealVersion: number;
|
revealVersion: number;
|
||||||
|
anchorServerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubscriptionFeatureOptions {
|
interface SubscriptionFeatureOptions {
|
||||||
@@ -464,9 +465,11 @@ function ProfileGroup({
|
|||||||
|
|
||||||
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
|
{localStatus && <div className="client-profile-local-status" role="status">{localStatus}</div>}
|
||||||
|
|
||||||
{!expanded && !closing && visibleServer && <button
|
{visibleServer && <button
|
||||||
className="client-profile-selected-server"
|
className="client-profile-selected-server"
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-expanded={expanded}
|
||||||
|
aria-controls={`client-profile-${profile.id}`}
|
||||||
onClick={() => feature.toggleProfile(profile.id)}
|
onClick={() => feature.toggleProfile(profile.id)}
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" />
|
<span aria-hidden="true" />
|
||||||
@@ -486,6 +489,7 @@ function ProfileGroup({
|
|||||||
disabled: controlsBlocked,
|
disabled: controlsBlocked,
|
||||||
leaving: false,
|
leaving: false,
|
||||||
revealVersion: feature.revealVersions[profile.id] || 0,
|
revealVersion: feature.revealVersions[profile.id] || 0,
|
||||||
|
anchorServerId: visibleServer?.id || '',
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px minmax(0, 1fr);
|
grid-template-columns: 18px minmax(0, 1fr);
|
||||||
|
column-gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 0 4px 8px;
|
padding: 4px 0 4px 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -278,15 +279,6 @@
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
animation: client-profile-selected-in 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes client-profile-selected-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
filter: blur(3px);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-profile-selected-server > span {
|
.client-profile-selected-server > span {
|
||||||
|
|||||||
@@ -131,7 +131,9 @@ test('server picker starts simple and reveals advanced controls on demand', () =
|
|||||||
assert.match(picker, /inert={!advanced \? true : undefined}/);
|
assert.match(picker, /inert={!advanced \? true : undefined}/);
|
||||||
assert.match(picker, /const SIMPLE_SERVER_LIMIT = 5/);
|
assert.match(picker, /const SIMPLE_SERVER_LIMIT = 5/);
|
||||||
assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/);
|
assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/);
|
||||||
assert.match(picker, /\{selected && <ServerRow/);
|
assert.match(picker, /selected && selected\.id !== anchorServerId && <ServerRow/);
|
||||||
|
assert.match(picker, /id !== selectedServerId && id !== anchorServerId/);
|
||||||
|
assert.match(overview, /anchorServerId=\{pickerState\.anchorServerId\}/);
|
||||||
assert.match(styles, /\.client-profile-body \.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 64px;/);
|
assert.match(styles, /\.client-profile-body \.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 64px;/);
|
||||||
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?background: color-mix\(in oklch, var\(--client-accent\) 2%, transparent\);/);
|
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?background: color-mix\(in oklch, var\(--client-accent\) 2%, transparent\);/);
|
||||||
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
|
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
|
||||||
|
|||||||
@@ -36,23 +36,23 @@ const expectedImports = [
|
|||||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||||
const acceptedLedger = {
|
const acceptedLedger = {
|
||||||
counts: {
|
counts: {
|
||||||
cascadeEdges: 821,
|
cascadeEdges: 820,
|
||||||
customProperties: 31,
|
customProperties: 31,
|
||||||
declarations: 2986,
|
declarations: 2983,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 57,
|
keyframes: 56,
|
||||||
media: 13,
|
media: 13,
|
||||||
rules: 921,
|
rules: 920,
|
||||||
variableReferences: 330,
|
variableReferences: 330,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: 'ac82341d26b6598c9e54a5cc2f20fd791baf0ab6a99c8bd09e838608a16567f7',
|
cascadeEdges: '1590610791a755c9ec3c64da5b56da82764c9906344819fae414bf871bab1bad',
|
||||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||||
declarations: 'a7b2ecccc36118b0f77b100a48b2fdf8d8653587090a73b527b9565c7f5a88b8',
|
declarations: '0bde16ab93623a2546daf42e83f037dc00d65dbf08da5851bf433d22697338b8',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
||||||
keyframes: '077e69647e0bd0761ca918e8d4109a9e8b07a1d7dbd9e0702bdca0e48011aed1',
|
keyframes: '39b6fc9c9ab451d63bb77afd90caf63d2d74a30cb351a8f86503de380d76a936',
|
||||||
ruleDeclarationSequences: '36ea3201b45a89f3607e9336d30e2d0e5b88400f921847fa28263c2a2b8b75ac',
|
ruleDeclarationSequences: '883a3996d9d4ccd4ab6124237b6e0c00f17505dc8ff7314a8c0069fa828a2475',
|
||||||
selectors: 'ec10ef7ce8f812c52c7ff6b6952ae66f5bbeebbc61e582ceddcb54c7112bbf2b',
|
selectors: 'ec10ef7ce8f812c52c7ff6b6952ae66f5bbeebbc61e582ceddcb54c7112bbf2b',
|
||||||
variableReferences: '8cb18a63664f73e6186e1730d3cd97c6e9f11bbe6b0f5cf04900948494a71f78',
|
variableReferences: '8cb18a63664f73e6186e1730d3cd97c6e9f11bbe6b0f5cf04900948494a71f78',
|
||||||
witnesses: '204a3ad7cb5b3681ea78a2e4ef1265515c33ca97ebfe560c082d51303cd3bedb',
|
witnesses: '204a3ad7cb5b3681ea78a2e4ef1265515c33ca97ebfe560c082d51303cd3bedb',
|
||||||
@@ -306,8 +306,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
|
|||||||
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
||||||
|
|
||||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||||
assert.deepEqual(assets, ['index-CgoReV4i.css']);
|
assert.deepEqual(assets, ['index-DgGVjCSB.css']);
|
||||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||||
assert.equal(built.byteLength, 112580);
|
assert.equal(built.byteLength, 112427);
|
||||||
assert.equal(sha256(built), 'cdfeb513550fab679856b62c6ecb3d9bab8cbc464a0ffaa6c707701f7c2078ae');
|
assert.equal(sha256(built), '70e5b945a85ac965b31f43c25cf5766a3860453090678c44fbcdffa9951d624e');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ test('profiles render as flat accordion groups with scoped controls', () => {
|
|||||||
assert.match(feature, /aria-expanded=\{expanded\}/);
|
assert.match(feature, /aria-expanded=\{expanded\}/);
|
||||||
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
|
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
|
||||||
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
|
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
|
||||||
assert.match(feature, /!expanded && !closing && visibleServer && <button[\s\S]*client-profile-selected-server/);
|
assert.match(feature, /\{visibleServer && <button[\s\S]*client-profile-selected-server[\s\S]*aria-expanded=\{expanded\}/);
|
||||||
|
assert.match(feature, /anchorServerId: visibleServer\?\.id \|\| ''/);
|
||||||
assert.match(feature, /const visibleServerId = applied \? feature\.selection\.appliedServerId : profile\.desiredServerId/);
|
assert.match(feature, /const visibleServerId = applied \? feature\.selection\.appliedServerId : profile\.desiredServerId/);
|
||||||
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
||||||
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);
|
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);
|
||||||
|
|||||||
Reference in New Issue
Block a user