chore(release): gate users-branch features for hotfix

Add USERS_BRANCH_FEATURES_ENABLED (off by default) to hide Players library, session tokens, and related UI while keeping the preview import fix. Restore primary Run button when the flag is off.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-04 10:15:28 +08:00
parent 4e6f7321b8
commit cbb6edc378
9 changed files with 148 additions and 59 deletions
+8 -3
View File
@@ -5,6 +5,7 @@ import { pickEraseTargetId } from '../../shared/effectEraserHitTest';
import { fitAspectRect } from '../../shared/geometry/fitAspectRect';
import { ipcChannels } from '../../shared/ipc/contracts';
import type { SessionState } from '../../shared/ipc/contracts';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import {
isNodeInMainStoryline,
isNodeInSideStoryline,
@@ -1778,6 +1779,8 @@ export function ControlApp() {
<div className={styles.previewHeader}>
<div className={styles.previewTitle}>{t('control.screenPreview')}</div>
<div className={styles.previewActions}>
{USERS_BRANCH_FEATURES_ENABLED ? (
<>
<label className={styles.npcTokenScale}>
<span className={styles.npcTokenScaleLabel}>{t('control.npcTokenScale')}</span>
<input
@@ -1816,6 +1819,8 @@ export function ControlApp() {
? t('control.hidePlayers')
: t('control.showPlayers')}
</Button>
) : null}
</>
) : null}
<Button onClick={() => void api.invoke(ipcChannels.windows.closeMultiWindow, {})}>
{t('control.stopPresentation')}
@@ -2026,7 +2031,7 @@ export function ControlApp() {
}}
/>
) : null}
{previewContentRect ? (
{USERS_BRANCH_FEATURES_ENABLED && previewContentRect ? (
<SceneNpcTokensOverlay
placements={currentScene?.npcTokens ?? []}
library={session?.project?.npcs ?? []}
@@ -2046,7 +2051,7 @@ export function ControlApp() {
}}
/>
) : null}
{previewContentRect ? (
{USERS_BRANCH_FEATURES_ENABLED && previewContentRect ? (
<ScenePlayerTokensOverlay
library={appPlayers}
session={scenePlayerTokensSession}
@@ -2509,7 +2514,7 @@ export function ControlApp() {
) : null}
</div>
{npcSessionCtxMenu
{USERS_BRANCH_FEATURES_ENABLED && npcSessionCtxMenu
? createPortal(
<>
<button
+31 -10
View File
@@ -2,6 +2,7 @@ import React, { startTransition, useCallback, useEffect, useMemo, useRef, useSta
import { createPortal } from 'react-dom';
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import type {
NpcImportResolution,
SceneImportResolution,
@@ -841,6 +842,7 @@ export function EditorApp() {
{t('top.file')}
</button>
) : null}
{USERS_BRANCH_FEATURES_ENABLED ? (
<button
type="button"
data-testid="players-header-btn"
@@ -858,6 +860,7 @@ export function EditorApp() {
>
{t('top.players')}
</button>
) : null}
</div>
<div className={styles.flex1} />
{appVersionText ? (
@@ -868,6 +871,7 @@ export function EditorApp() {
<div className={styles.headerActions}>
{state.project ? (
<>
{USERS_BRANCH_FEATURES_ENABLED ? (
<div
ref={runSplitRef}
className={styles.splitRun}
@@ -917,6 +921,19 @@ export function EditorApp() {
</svg>
</button>
</div>
) : (
<Button
variant="primary"
disabled={runDisabled || launching}
data-testid="run-main-btn"
onClick={() => {
if (!licenseActive || !graphStartGraphNodeId || launching) return;
launchFromGraphNode(graphStartGraphNodeId);
}}
>
{t('top.run')}
</Button>
)}
{runDisabled ? (
<Button
variant="ghost"
@@ -1620,16 +1637,20 @@ export function EditorApp() {
}}
/>
<CheckUpdatesModal open={checkUpdatesOpen} onClose={() => setCheckUpdatesOpen(false)} />
<PlayersManagerModal open={playersManagerOpen} onClose={() => setPlayersManagerOpen(false)} />
<LaunchPlayersModal
open={launchPlayersOpen}
onClose={() => setLaunchPlayersOpen(false)}
onConfirm={(playerIds) => {
if (!graphStartGraphNodeId) return;
launchFromGraphNode(graphStartGraphNodeId, playerIds);
}}
/>
{runMenuOpen && runMenuPos
{USERS_BRANCH_FEATURES_ENABLED ? (
<>
<PlayersManagerModal open={playersManagerOpen} onClose={() => setPlayersManagerOpen(false)} />
<LaunchPlayersModal
open={launchPlayersOpen}
onClose={() => setLaunchPlayersOpen(false)}
onConfirm={(playerIds) => {
if (!graphStartGraphNodeId) return;
launchFromGraphNode(graphStartGraphNodeId, playerIds);
}}
/>
</>
) : null}
{USERS_BRANCH_FEATURES_ENABLED && runMenuOpen && runMenuPos
? createPortal(
<div
data-runmenu-root="1"
+8 -2
View File
@@ -1,5 +1,7 @@
/** Порядок разделов в окне «Инструкция». */
export const HELP_SECTION_IDS = [
import { USERS_BRANCH_FEATURES_ENABLED } from '../../../shared/features/usersBranchFeatures';
const HELP_SECTION_IDS_ALL = [
'overview',
'license',
'projects',
@@ -25,7 +27,11 @@ export const HELP_SECTION_IDS = [
'settings',
] as const;
export type HelpSectionId = (typeof HELP_SECTION_IDS)[number];
export type HelpSectionId = (typeof HELP_SECTION_IDS_ALL)[number];
export const HELP_SECTION_IDS: readonly HelpSectionId[] = USERS_BRANCH_FEATURES_ENABLED
? HELP_SECTION_IDS_ALL
: HELP_SECTION_IDS_ALL.filter((id) => id !== 'players');
export function helpSectionTitleKey(id: HelpSectionId): string {
return `help.section.${id}.title`;
+32 -21
View File
@@ -14,6 +14,7 @@ import type {
import editorStyles from '../editor/EditorApp.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { getDndApi } from '../shared/dndApi';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import { PlayerTokenView } from '../shared/playerToken/PlayerTokenView';
import { Button, Input, Select } from '../shared/ui/controls';
import { useAssetUrl } from '../shared/useAssetImageUrl';
@@ -737,27 +738,35 @@ export function NpcsEditorApp() {
<div>
<div className={styles.fieldLabel}>{t('npcs.avatar')}</div>
<div className={styles.avatarPick}>
<PlayerTokenView
name={selected.name}
imageUrl={selectedUrl}
ringColor={npcDispositionRingColor(selected.disposition ?? 'neutral')}
imageOffset={selected.imageOffset}
imageScale={selected.imageScale}
sizePx={140}
panEnabled
onImageOffsetChange={(imageOffset) => {
void api.invoke(ipcChannels.project.updateNpcFields, {
npcId: selected.id,
imageOffset,
});
}}
onImageScaleChange={(imageScale) => {
void api.invoke(ipcChannels.project.updateNpcFields, {
npcId: selected.id,
imageScale,
});
}}
/>
{USERS_BRANCH_FEATURES_ENABLED ? (
<PlayerTokenView
name={selected.name}
imageUrl={selectedUrl}
ringColor={npcDispositionRingColor(selected.disposition ?? 'neutral')}
imageOffset={selected.imageOffset}
imageScale={selected.imageScale}
sizePx={140}
panEnabled
onImageOffsetChange={(imageOffset) => {
void api.invoke(ipcChannels.project.updateNpcFields, {
npcId: selected.id,
imageOffset,
});
}}
onImageScaleChange={(imageScale) => {
void api.invoke(ipcChannels.project.updateNpcFields, {
npcId: selected.id,
imageScale,
});
}}
/>
) : (
<div className={styles.avatarPreview}>
{selectedUrl ? (
<img className={styles.avatarPreviewImg} src={selectedUrl} alt="" />
) : null}
</div>
)}
<Button
disabled={avatarBusy}
onClick={() => {
@@ -782,6 +791,7 @@ export function NpcsEditorApp() {
</div>
</div>
{USERS_BRANCH_FEATURES_ENABLED ? (
<label>
<div className={styles.fieldLabel}>{t('npcs.disposition')}</div>
<Select
@@ -800,6 +810,7 @@ export function NpcsEditorApp() {
]}
/>
</label>
) : null}
<div>
<div className={styles.fieldLabel}>{t('npcs.name')}</div>
+6 -3
View File
@@ -44,6 +44,7 @@ import {
trapTypeLabelRu,
} from '../../shared/types/sceneTraps';
import { sceneViewPanBy, sceneViewZoomAt } from '../../shared/types/sceneView';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import editorStyles from '../editor/EditorApp.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { getDndApi } from '../shared/dndApi';
@@ -484,7 +485,7 @@ export function SceneEditorApp() {
return;
}
const npcId = e.dataTransfer.getData(SCENE_NPC_DND_MIME);
if (npcId) {
if (USERS_BRANCH_FEATURES_ENABLED && npcId) {
addNpcTokenAt(npcId as NpcId, p.x, p.y);
return;
}
@@ -600,6 +601,7 @@ export function SceneEditorApp() {
) : null}
</div>
{USERS_BRANCH_FEATURES_ENABLED ? (
<div className={styles.accordion} data-testid="scene-npc-accordion">
<button type="button" className={styles.accordionHead} onClick={() => setNpcsOpen((v) => !v)}>
НПС {npcsOpen ? '▾' : '▸'}
@@ -621,6 +623,7 @@ export function SceneEditorApp() {
</div>
) : null}
</div>
) : null}
<div className={styles.accordion}>
<button type="button" className={styles.accordionHead} onClick={() => setTokensOpen((v) => !v)}>
@@ -884,7 +887,7 @@ export function SceneEditorApp() {
);
})
: null}
{contentRect
{USERS_BRANCH_FEATURES_ENABLED && contentRect
? localNpcTokens.map((tok) => {
const npc = project?.npcs.find((item) => item.id === tok.npcId);
if (!npc) return null;
@@ -1050,7 +1053,7 @@ export function SceneEditorApp() {
)
: null}
{npcCtxMenu
{USERS_BRANCH_FEATURES_ENABLED && npcCtxMenu
? createPortal(
<>
<button
+5 -3
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { computeTimeSec } from '../../main/video/videoPlaybackStore';
import type { SessionState } from '../../shared/ipc/contracts';
import { USERS_BRANCH_FEATURES_ENABLED } from '../../shared/features/usersBranchFeatures';
import { ExplosionVideoOverlay } from './effects/ExplosionVideoOverlay';
import { PixiEffectsOverlay } from './effects/PxiEffectsOverlay';
@@ -54,7 +55,8 @@ export function PresentationView({
const [materialsOverlay] = useMaterialsOverlayState();
const [npcsOverlay] = useNpcsOverlayState();
const appTokens = useAppTokens();
const { players: appPlayers } = useAppPlayers();
const appPlayersResult = useAppPlayers();
const appPlayers = USERS_BRANCH_FEATURES_ENABLED ? appPlayersResult.players : [];
const [sceneTokensSession] = useSceneTokensSession();
const [sceneNpcTokensSession] = useSceneNpcTokensSession();
const [scenePlayerTokensSession] = useScenePlayerTokensSession();
@@ -196,7 +198,7 @@ export function PresentationView({
viewport={contentRect}
/>
) : null}
{scene?.previewAssetType === 'image' && contentRect ? (
{USERS_BRANCH_FEATURES_ENABLED && scene?.previewAssetType === 'image' && contentRect ? (
<SceneNpcTokensOverlay
placements={scene.npcTokens ?? []}
library={project?.npcs ?? []}
@@ -205,7 +207,7 @@ export function PresentationView({
grid={scene.grid}
/>
) : null}
{scene?.previewAssetType === 'image' && contentRect ? (
{USERS_BRANCH_FEATURES_ENABLED && scene?.previewAssetType === 'image' && contentRect ? (
<ScenePlayerTokensOverlay
library={appPlayers}
session={scenePlayerTokensSession}