Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements. Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
export interface ConfigSource {
|
||
id: number | null;
|
||
name: string;
|
||
version: number | null;
|
||
kind: 'baseline' | 'config' | 'map';
|
||
draft?: boolean;
|
||
}
|
||
|
||
export interface ConfigAssignment {
|
||
value: string;
|
||
source: ConfigSource;
|
||
line: number;
|
||
}
|
||
|
||
export interface ConfigSetting {
|
||
key: string;
|
||
value: string;
|
||
source: ConfigSource;
|
||
history: ConfigAssignment[];
|
||
}
|
||
|
||
export interface ConfigInspection {
|
||
text: string;
|
||
settings: ConfigSetting[];
|
||
sources: ConfigSource[];
|
||
warnings: string[];
|
||
reason?: string;
|
||
}
|
||
|
||
export interface ConfigServer { name: string; game: string | null; map: string | null; }
|
||
|
||
/** The same lowercase `set` grammar that the overlay composer accepts. */
|
||
export function configLines(text: string) {
|
||
return text.split(/\r?\n/).flatMap((line, index) => {
|
||
const match = /^(\s*set\s+)(\S+)(\s+)(.*)$/.exec(line);
|
||
return match ? [{ index, key: match[2], value: match[4], prefix: match[1] + match[2] + match[3] }] : [];
|
||
});
|
||
}
|
||
|
||
export function changeConfigValue(text: string, index: number, value: string): string {
|
||
// Keep every separator, comment and unrelated line byte-for-byte intact.
|
||
const parts = text.split(/(\r?\n)/);
|
||
const entry = configLines(text).find(line => line.index === index);
|
||
if (entry) parts[index * 2] = entry.prefix + value.replace(/[\r\n]/g, '');
|
||
return parts.join('');
|
||
}
|
||
|
||
/** Add one assignment without changing existing commands or line endings. */
|
||
export function addConfigSetting(text: string, key: string, value: string): string {
|
||
key = key.trim(); value = value.trim();
|
||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) throw new Error('Use a setting name containing letters, numbers, and underscores.');
|
||
if (!value || /[\r\n]/.test(value) || value.startsWith('//')) throw new Error('Enter a value on one line.');
|
||
if (configLines(text).some(entry => entry.key.toLowerCase() === key.toLowerCase())) throw new Error('This setting already exists. Find it above to edit its value.');
|
||
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
||
const formatted = /\s/.test(value) && !value.startsWith('"') ? JSON.stringify(value) : value;
|
||
return text + (text && !text.endsWith('\n') ? newline : '') + `set ${key} ${formatted}` + newline;
|
||
}
|
||
|
||
export function configWarnings(text: string, overlay: boolean): string[] {
|
||
const warnings: string[] = [];
|
||
const seen = new Map<string, number>();
|
||
for (const entry of configLines(text)) {
|
||
const key = entry.key.toLowerCase();
|
||
if (seen.has(key)) warnings.push(`Line ${entry.index + 1}: ${entry.key} is also assigned on line ${seen.get(key)}.`);
|
||
seen.set(key, entry.index + 1);
|
||
if (!entry.value.trim() || entry.value.trim().startsWith('//')) warnings.push(`Line ${entry.index + 1}: ${entry.key} has no value.`);
|
||
}
|
||
if (overlay) text.split(/\r?\n/).forEach((line, index) => {
|
||
if (line.trim() && !/^\s*\/\//.test(line) && !/^\s*set\s+\S+\s+/.test(line)) {
|
||
warnings.push(`Line ${index + 1} is ignored by overlays. Use set KEY VALUE.`);
|
||
}
|
||
});
|
||
if (/^\s*(?:(?:set|seta)\s+)?sv_maprotation\s+/im.test(text)) {
|
||
warnings.push('For recognized maps, the server’s selected map overrides sv_maprotation.');
|
||
}
|
||
return warnings;
|
||
}
|