mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-23 12:08:28 +01:00
port monkey changelog update [No GBP] (#19026)
* port monkey chnagelog update * type safe * . * functions * whyi s that even there
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
/datum/changelog
|
||||
var/static/list/changelog_items = list()
|
||||
var/static/list/dates
|
||||
var/static/list/testmerges
|
||||
|
||||
/datum/changelog/tgui_state()
|
||||
return GLOB.tgui_always_state
|
||||
|
||||
/datum/changelog/tgui_interact(mob/user, datum/tgui/ui)
|
||||
if(isnull(dates))
|
||||
dates = get_dates()
|
||||
if(isnull(testmerges))
|
||||
testmerges = get_testmerge_data()
|
||||
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if (!ui)
|
||||
ui = new(user, src, "Changelog")
|
||||
ui.set_autoupdate(FALSE)
|
||||
ui.open()
|
||||
|
||||
/datum/changelog/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
@@ -22,11 +30,62 @@
|
||||
return ui.send_asset(changelog_item)
|
||||
|
||||
/datum/changelog/tgui_static_data()
|
||||
var/list/data = list( "dates" = list() )
|
||||
var/regex/ymlRegex = regex(@"\.yml", "g")
|
||||
var/list/data = list()
|
||||
|
||||
for(var/archive_file in sortList(flist("html/changelogs/archive/")))
|
||||
var/archive_date = ymlRegex.Replace(archive_file, "")
|
||||
data["dates"] = list(archive_date) + data["dates"]
|
||||
data["dates"] = dates
|
||||
data["testmerges"] = testmerges
|
||||
data["discord_url"] = CONFIG_GET(string/discordurl)
|
||||
|
||||
return data
|
||||
|
||||
/datum/changelog/proc/get_dates()
|
||||
. = list()
|
||||
var/regex/yml_regex = regex(@"\.yml", "g")
|
||||
for(var/archive_file in flist("html/changelogs/archive/"))
|
||||
. += replacetext(archive_file, yml_regex, "")
|
||||
|
||||
/datum/changelog/proc/get_testmerge_data()
|
||||
. = list()
|
||||
for(var/datum/tgs_revision_information/test_merge/testmerge in world.TgsTestMerges())
|
||||
if(!testmerge.body || findtext(testmerge.title, @"[s]"))
|
||||
continue
|
||||
var/list/changes = parse_github_changelog(testmerge.body)
|
||||
if(!length(changes))
|
||||
changes = list("unknown" = list("Changes are not documented. Ask the author ([testmerge.author]) to add a changelog to their PR!"))
|
||||
var/list/testmerge_data = list(
|
||||
"title" = trimtext("[testmerge.title]"),
|
||||
"number" = testmerge.number,
|
||||
"author" = testmerge.author,
|
||||
"link" = testmerge.url,
|
||||
"changes" = changes,
|
||||
)
|
||||
. += list(testmerge_data)
|
||||
|
||||
/proc/parse_github_changelog(body) as /list
|
||||
// these shouldn't be static, as this will only be called a few times at most in get_testmerge_data, which will only be called once.
|
||||
var/regex/cl_pattern = new(@"(:cl:|🆑)([\S \t]*)$")
|
||||
var/regex/entry_pattern = new(@"(\w+): (.+)")
|
||||
var/regex/end_pattern = new(@"^/(:cl:|🆑)")
|
||||
var/regex/newline_pattern = new(@"(\r\n|\r|\n)")
|
||||
|
||||
// no changes, this is just using the default template
|
||||
if(findtext_char(body, "add: Added new mechanics or gameplay changes"))
|
||||
return
|
||||
|
||||
var/started = FALSE
|
||||
var/list/lines = splittext_char(trimtext(body), newline_pattern)
|
||||
|
||||
for(var/line in lines)
|
||||
line = trimtext(line)
|
||||
if(findtext_char(line, end_pattern))
|
||||
break
|
||||
if(started)
|
||||
if (findtext_char(line, entry_pattern))
|
||||
var/change_type = trimtext(entry_pattern.group[1])
|
||||
var/change_desc = trimtext(entry_pattern.group[2])
|
||||
if(!change_type || !change_desc)
|
||||
continue
|
||||
LAZYADDASSOCLIST(., change_type, change_desc)
|
||||
else
|
||||
if(findtext_char(line, cl_pattern))
|
||||
started = TRUE
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
import dateformat from 'dateformat';
|
||||
import yaml from 'js-yaml';
|
||||
import { Component, Fragment } from 'react';
|
||||
import { resolveAsset } from 'tgui/assets';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dropdown,
|
||||
Icon,
|
||||
Section,
|
||||
Stack,
|
||||
Table,
|
||||
} from 'tgui-core/components';
|
||||
import { classes } from 'tgui-core/react';
|
||||
import { sendAct as act } from '../events/act';
|
||||
|
||||
const icons = {
|
||||
add: { icon: 'check-circle', color: 'green' },
|
||||
admin: { icon: 'user-shield', color: 'purple' },
|
||||
balance: { icon: 'balance-scale-right', color: 'yellow' },
|
||||
bugfix: { icon: 'bug', color: 'green' },
|
||||
code_imp: { icon: 'code', color: 'green' },
|
||||
config: { icon: 'cogs', color: 'purple' },
|
||||
expansion: { icon: 'check-circle', color: 'green' },
|
||||
experiment: { icon: 'radiation', color: 'yellow' },
|
||||
image: { icon: 'image', color: 'green' },
|
||||
imageadd: { icon: 'tg-image-plus', color: 'green' },
|
||||
imagedel: { icon: 'tg-image-minus', color: 'red' },
|
||||
qol: { icon: 'hand-holding-heart', color: 'green' },
|
||||
refactor: { icon: 'tools', color: 'green' },
|
||||
rscadd: { icon: 'check-circle', color: 'green' },
|
||||
rscdel: { icon: 'times-circle', color: 'red' },
|
||||
server: { icon: 'server', color: 'purple' },
|
||||
sound: { icon: 'volume-high', color: 'green' },
|
||||
soundadd: { icon: 'tg-sound-plus', color: 'green' },
|
||||
sounddel: { icon: 'tg-sound-minus', color: 'red' },
|
||||
spellcheck: { icon: 'spell-check', color: 'green' },
|
||||
tgs: { icon: 'toolbox', color: 'purple' },
|
||||
tweak: { icon: 'wrench', color: 'green' },
|
||||
unknown: { icon: 'info-circle', color: 'label' },
|
||||
wip: { icon: 'hammer', color: 'orange' },
|
||||
};
|
||||
|
||||
type Data = { dates: string[] };
|
||||
|
||||
type ChangelogProps = Record<never, never>;
|
||||
|
||||
interface ChangelogState {
|
||||
data: string | { date: string; authors: { name: string; changes: string[] } };
|
||||
selectedDate: string;
|
||||
selectedIndex: number;
|
||||
}
|
||||
export class Changelog extends Component<ChangelogProps, ChangelogState> {
|
||||
dateChoices: string[];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: 'Loading changelog data...',
|
||||
selectedDate: '',
|
||||
selectedIndex: 0,
|
||||
};
|
||||
this.dateChoices = [];
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.setState({ data });
|
||||
}
|
||||
|
||||
setSelectedDate(selectedDate) {
|
||||
this.setState({ selectedDate });
|
||||
}
|
||||
|
||||
setSelectedIndex(selectedIndex) {
|
||||
this.setState({ selectedIndex });
|
||||
}
|
||||
|
||||
getData = (date, attemptNumber = 1) => {
|
||||
const self = this;
|
||||
const maxAttempts = 6;
|
||||
|
||||
if (attemptNumber > maxAttempts) {
|
||||
return this.setData(`Failed to load data after ${maxAttempts} attempts`);
|
||||
}
|
||||
|
||||
act('get_month', { date });
|
||||
|
||||
fetch(resolveAsset(`${date}.yml`)).then(async (changelogData) => {
|
||||
const result = await changelogData.text();
|
||||
const errorRegex = /^Cannot find/;
|
||||
|
||||
if (errorRegex.test(result)) {
|
||||
const timeout = 50 + attemptNumber * 50;
|
||||
|
||||
self.setData(`Loading changelog data${'.'.repeat(attemptNumber + 3)}`);
|
||||
setTimeout(() => {
|
||||
self.getData(date, attemptNumber + 1);
|
||||
}, timeout);
|
||||
} else {
|
||||
self.setData(yaml.load(result, { schema: yaml.CORE_SCHEMA }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
data: { dates = [] },
|
||||
} = useBackend<Data>();
|
||||
|
||||
if (dates) {
|
||||
dates.forEach((date) => {
|
||||
this.dateChoices.push(dateformat(date, 'mmmm yyyy', true));
|
||||
});
|
||||
this.setSelectedDate(this.dateChoices[0]);
|
||||
this.getData(dates[0]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, selectedDate, selectedIndex } = this.state;
|
||||
const {
|
||||
data: { dates },
|
||||
} = useBackend<Data>();
|
||||
const { dateChoices } = this;
|
||||
|
||||
const dateDropdown = dateChoices.length > 0 && (
|
||||
<Stack mb={1}>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
className="Changelog__Button"
|
||||
disabled={selectedIndex === 0}
|
||||
icon={'chevron-left'}
|
||||
onClick={() => {
|
||||
const index = selectedIndex - 1;
|
||||
|
||||
this.setData('Loading changelog data...');
|
||||
this.setSelectedIndex(index);
|
||||
this.setSelectedDate(dateChoices[index]);
|
||||
window.scrollTo(
|
||||
0,
|
||||
document.body.scrollHeight ||
|
||||
document.documentElement.scrollHeight,
|
||||
);
|
||||
return this.getData(dates[index]);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
displayText={selectedDate}
|
||||
options={dateChoices}
|
||||
onSelected={(value) => {
|
||||
const index = dateChoices.indexOf(value);
|
||||
|
||||
this.setData('Loading changelog data...');
|
||||
this.setSelectedIndex(index);
|
||||
this.setSelectedDate(value);
|
||||
window.scrollTo(
|
||||
0,
|
||||
document.body.scrollHeight ||
|
||||
document.documentElement.scrollHeight,
|
||||
);
|
||||
return this.getData(dates[index]);
|
||||
}}
|
||||
selected={selectedDate}
|
||||
width={'150px'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
className="Changelog__Button"
|
||||
disabled={selectedIndex === dateChoices.length - 1}
|
||||
icon={'chevron-right'}
|
||||
onClick={() => {
|
||||
const index = selectedIndex + 1;
|
||||
|
||||
this.setData('Loading changelog data...');
|
||||
this.setSelectedIndex(index);
|
||||
this.setSelectedDate(dateChoices[index]);
|
||||
window.scrollTo(
|
||||
0,
|
||||
document.body.scrollHeight ||
|
||||
document.documentElement.scrollHeight,
|
||||
);
|
||||
return this.getData(dates[index]);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const header = (
|
||||
<Section>
|
||||
<h1>VOREStation Changelist</h1>
|
||||
<p>
|
||||
{'The GitHub repository can be found '}
|
||||
<a href="https://github.com/VOREStation/VOREStation">here</a>
|
||||
{', recent GitHub contributors can be found '}
|
||||
<a href="https://github.com/VOREStation/VOREStation/pulse/monthly">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
{'Visit our wiki '}
|
||||
<a href="https://wiki.vore-station.net/Main_Page">here</a>
|
||||
{', check out our discord server '}
|
||||
<a href="https://discord.gg/Zd5WMuq">here</a>.
|
||||
</p>
|
||||
{dateDropdown}
|
||||
</Section>
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<Section>
|
||||
{dateDropdown}
|
||||
<h3>VOREStation License</h3>
|
||||
<p>
|
||||
{'All code after '}
|
||||
<a
|
||||
href={
|
||||
'https://github.com/VOREStation/VOREStation/commit/' +
|
||||
'333c566b88108de218d882840e61928a9b759d8f'
|
||||
}
|
||||
>
|
||||
commit 333c566b88108de218d882840e61928a9b759d8f on 2014/31/12 at
|
||||
4:38 PM PST
|
||||
</a>
|
||||
{' is licensed under '}
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU AGPL v3</a>
|
||||
{'. All code before that commit is licensed under '}
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU GPL v3</a>
|
||||
{', including tools unless their readme specifies otherwise. See '}
|
||||
<a href="https://github.com/VOREStation/VOREStation/blob/master/LICENSE">
|
||||
LICENSE
|
||||
</a>
|
||||
{' and '}
|
||||
<a href="https://github.com/VOREStation/VOREStation/blob/master/LICENSE-GPL3.txt">
|
||||
GPLv3.txt
|
||||
</a>
|
||||
{' for more details.'}
|
||||
</p>
|
||||
<p>
|
||||
The TGS DMAPI API is licensed as a subproject under the MIT license.
|
||||
{' See the footer of '}
|
||||
<a
|
||||
href={
|
||||
'https://github.com/VOREStation/VOREStation/blob/master' +
|
||||
'/code/__DEFINES/tgs.dm'
|
||||
}
|
||||
>
|
||||
code/__DEFINES/tgs.dm
|
||||
</a>
|
||||
{' and '}
|
||||
<a
|
||||
href={
|
||||
'https://github.com/VOREStation/VOREStation/blob/master' +
|
||||
'/code/modules/tgs/LICENSE'
|
||||
}
|
||||
>
|
||||
code/modules/tgs/LICENSE
|
||||
</a>
|
||||
{' for the MIT license.'}
|
||||
</p>
|
||||
<p>
|
||||
{'All assets including icons and sound are under a '}
|
||||
<a href="https://creativecommons.org/licenses/by-sa/3.0/">
|
||||
Creative Commons 3.0 BY-SA license
|
||||
</a>
|
||||
{' unless otherwise indicated.'}
|
||||
</p>
|
||||
</Section>
|
||||
);
|
||||
|
||||
const changes =
|
||||
typeof data === 'object' &&
|
||||
Object.keys(data).length > 0 &&
|
||||
Object.entries(data)
|
||||
.reverse()
|
||||
.map(([date, authors]) => (
|
||||
<Section key={date} title={dateformat(date, 'd mmmm yyyy', true)}>
|
||||
<Box ml={3}>
|
||||
{Object.entries(authors).map(([name, changes]) => (
|
||||
<Fragment key={name}>
|
||||
<h4>{name} changed:</h4>
|
||||
<Box ml={3}>
|
||||
<Table>
|
||||
{(changes as string[]).map((change) => {
|
||||
const changeType = Object.keys(change)[0];
|
||||
return (
|
||||
<Table.Row key={changeType + change[changeType]}>
|
||||
<Table.Cell
|
||||
className={classes([
|
||||
'Changelog__Cell',
|
||||
'Changelog__Cell--Icon',
|
||||
])}
|
||||
>
|
||||
<Icon
|
||||
color={
|
||||
icons[changeType]
|
||||
? icons[changeType].color
|
||||
: icons.unknown.icon
|
||||
}
|
||||
name={
|
||||
icons[changeType]
|
||||
? icons[changeType].icon
|
||||
: icons.unknown.icon
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="Changelog__Cell">
|
||||
{change[changeType]}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</Box>
|
||||
</Fragment>
|
||||
))}
|
||||
</Box>
|
||||
</Section>
|
||||
));
|
||||
|
||||
return (
|
||||
<Window title="Changelog" width={675} height={650}>
|
||||
<Window.Content scrollable>
|
||||
{header}
|
||||
{changes}
|
||||
{typeof data === 'string' && <p>{data}</p>}
|
||||
{footer}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Icon, Table } from 'tgui-core/components';
|
||||
import { classes } from 'tgui-core/react';
|
||||
import { icons } from './constatnts';
|
||||
|
||||
export const ChangeRow = (props: { kind: string; content: string }) => {
|
||||
return (
|
||||
<Table.Row>
|
||||
<Table.Cell
|
||||
className={classes(['Changelog__Cell', 'Changelog__Cell--Icon'])}
|
||||
>
|
||||
<Icon
|
||||
color={
|
||||
icons[props.kind] ? icons[props.kind].color : icons.unknown.color
|
||||
}
|
||||
name={icons[props.kind] ? icons[props.kind].icon : icons.unknown.icon}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="Changelog__Cell">{props.content}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import dateformat from 'dateformat';
|
||||
import { Fragment } from 'react';
|
||||
import { Box, Section, Table } from 'tgui-core/components';
|
||||
import { ChangeRow } from './ChangeRow';
|
||||
import type { ChangelogEntry } from './types';
|
||||
|
||||
export const Changes = (props: { data: ChangelogEntry | string }) => {
|
||||
const { data } = props;
|
||||
|
||||
if (typeof data !== 'object' || Object.keys(data).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(data)
|
||||
.reverse()
|
||||
.map(([date, authors]) => (
|
||||
<Section key={date} title={dateformat(date, 'd mmmm yyyy', true)}>
|
||||
<Box ml={3}>
|
||||
{Object.entries(authors).map(([name, changes]) => (
|
||||
<Fragment key={name}>
|
||||
<h4>{name} changed:</h4>
|
||||
<Box ml={3}>
|
||||
<Table>
|
||||
{changes.map((change) =>
|
||||
Object.entries(change).map(([changeType, content]) => (
|
||||
<ChangeRow
|
||||
key={changeType + content}
|
||||
kind={changeType}
|
||||
content={content}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Table>
|
||||
</Box>
|
||||
</Fragment>
|
||||
))}
|
||||
</Box>
|
||||
</Section>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Button, Dropdown, Stack } from 'tgui-core/components';
|
||||
|
||||
export const DateDropdown = (props: {
|
||||
selectedIndex: number;
|
||||
setData: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSelectedIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
selectedDate: string;
|
||||
setSelectedDate: React.Dispatch<React.SetStateAction<string>>;
|
||||
dateChoices: string[];
|
||||
sortedDates: string[];
|
||||
getData: (date: string, attemptNumber?: number) => any;
|
||||
}) => {
|
||||
const {
|
||||
selectedIndex,
|
||||
setData,
|
||||
setSelectedIndex,
|
||||
selectedDate,
|
||||
setSelectedDate,
|
||||
dateChoices,
|
||||
sortedDates,
|
||||
getData,
|
||||
} = props;
|
||||
|
||||
if (!dateChoices.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleSelect(date: string) {
|
||||
const index = dateChoices.indexOf(date);
|
||||
if (index === -1) return;
|
||||
|
||||
setData('Loading changelog data...');
|
||||
setSelectedIndex(index);
|
||||
setSelectedDate(dateChoices[index]);
|
||||
getData(sortedDates[index]);
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
if (selectedIndex >= dateChoices.length - 1) return;
|
||||
handleSelect(dateChoices[selectedIndex + 1]);
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (selectedIndex <= 0) return;
|
||||
handleSelect(dateChoices[selectedIndex - 1]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack mb={1}>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
className="Changelog__Button"
|
||||
disabled={selectedIndex === dateChoices.length - 1}
|
||||
icon="chevron-left"
|
||||
onClick={handlePrev}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
displayText={selectedDate}
|
||||
options={dateChoices}
|
||||
onSelected={handleSelect}
|
||||
selected={selectedDate}
|
||||
width="150px"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
className="Changelog__Button"
|
||||
disabled={selectedIndex === 0}
|
||||
icon="chevron-right"
|
||||
onClick={handleNext}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Section } from 'tgui-core/components';
|
||||
|
||||
export const Footer = (props: { dateDropdown: ReactNode | null }) => {
|
||||
const { dateDropdown } = props;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{dateDropdown}
|
||||
<h3>VOREStation License</h3>
|
||||
<p>
|
||||
{'All code after '}
|
||||
<a
|
||||
href={
|
||||
'https://github.com/VOREStation/VOREStation/commit/' +
|
||||
'333c566b88108de218d882840e61928a9b759d8f'
|
||||
}
|
||||
>
|
||||
commit 333c566b88108de218d882840e61928a9b759d8f on 2014/31/12 at 4:38
|
||||
PM PST
|
||||
</a>
|
||||
{' is licensed under '}
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html">GNU AGPL v3</a>
|
||||
{'. All code before that commit is licensed under '}
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU GPL v3</a>
|
||||
{', including tools unless their readme specifies otherwise. See '}
|
||||
<a href="https://github.com/VOREStation/VOREStation/blob/master/LICENSE">
|
||||
LICENSE
|
||||
</a>
|
||||
{' and '}
|
||||
<a href="https://github.com/VOREStation/VOREStation/blob/master/LICENSE-GPL3.txt">
|
||||
GPLv3.txt
|
||||
</a>
|
||||
{' for more details.'}
|
||||
</p>
|
||||
<p>
|
||||
The TGS DMAPI API is licensed as a subproject under the MIT license.
|
||||
{' See the footer of '}
|
||||
<a
|
||||
href={
|
||||
'https://github.com/VOREStation/VOREStation/blob/master' +
|
||||
'/code/__DEFINES/tgs.dm'
|
||||
}
|
||||
>
|
||||
code/__DEFINES/tgs.dm
|
||||
</a>
|
||||
{' and '}
|
||||
<a
|
||||
href={
|
||||
'https://github.com/VOREStation/VOREStation/blob/master' +
|
||||
'/code/modules/tgs/LICENSE'
|
||||
}
|
||||
>
|
||||
code/modules/tgs/LICENSE
|
||||
</a>
|
||||
{' for the MIT license.'}
|
||||
</p>
|
||||
<p>
|
||||
{'All assets including icons and sound are under a '}
|
||||
<a href="https://creativecommons.org/licenses/by-sa/3.0/">
|
||||
Creative Commons 3.0 BY-SA license
|
||||
</a>
|
||||
{' unless otherwise indicated.'}
|
||||
</p>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Section } from 'tgui-core/components';
|
||||
|
||||
export const Header = (props: { dateDropdown: ReactNode | null }) => {
|
||||
const { dateDropdown } = props;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<h1>VOREStation Changelist</h1>
|
||||
<p>
|
||||
{'The GitHub repository can be found '}
|
||||
<a href="https://github.com/VOREStation/VOREStation">here</a>
|
||||
{', recent GitHub contributors can be found '}
|
||||
<a href="https://github.com/VOREStation/VOREStation/pulse/monthly">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
{'Visit our wiki '}
|
||||
<a href="https://wiki.vore-station.net/Main_Page">here</a>
|
||||
{', check out our discord server '}
|
||||
<a href="https://discord.gg/Zd5WMuq">here</a>.
|
||||
</p>
|
||||
{dateDropdown}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Collapsible, Section, Stack, Table } from 'tgui-core/components';
|
||||
import { ChangeRow } from './ChangeRow';
|
||||
import type { ChangelogData } from './types';
|
||||
|
||||
export const Testmerges = (_props) => {
|
||||
const {
|
||||
data: { testmerges },
|
||||
} = useBackend<ChangelogData>();
|
||||
|
||||
if (testmerges.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section px={1}>
|
||||
<h4>
|
||||
These are features being actively tested and developed on the server.
|
||||
Please report any issues or feedback to the original PR, or on the
|
||||
feedback thread on the Discord if there is one.
|
||||
</h4>
|
||||
</Section>
|
||||
<Stack vertical>
|
||||
{testmerges.map((testmerge) => {
|
||||
const title = (
|
||||
<a href={testmerge.link}>
|
||||
#{testmerge.number}: "{testmerge.title}" by{' '}
|
||||
{testmerge.author}
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<Stack.Item key={testmerge.number}>
|
||||
<Section title={title}>
|
||||
<Collapsible color="transparent" title="Changelog" open>
|
||||
<Box ml={3}>
|
||||
<Table>
|
||||
{Object.entries(testmerge.changes).map(
|
||||
([kind, changes]) =>
|
||||
changes.map((desc) => (
|
||||
<ChangeRow
|
||||
key={kind + desc}
|
||||
kind={kind}
|
||||
content={desc}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</Table>
|
||||
</Box>
|
||||
</Collapsible>
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
export const icons = {
|
||||
bugfix: { icon: 'bug', color: 'green' },
|
||||
wip: { icon: 'hammer', color: 'orange' },
|
||||
qol: { icon: 'hand-holding-heart', color: 'green' },
|
||||
sound: { icon: 'volume-high', color: 'green' },
|
||||
soundadd: { icon: 'tg-sound-plus', color: 'green' },
|
||||
sounddel: { icon: 'tg-sound-minus', color: 'red' },
|
||||
add: { icon: 'check-circle', color: 'green' },
|
||||
expansion: { icon: 'check-circle', color: 'green' },
|
||||
rscadd: { icon: 'check-circle', color: 'green' },
|
||||
rscdel: { icon: 'times-circle', color: 'red' },
|
||||
image: { icon: 'image', color: 'green' },
|
||||
imageadd: { icon: 'tg-image-plus', color: 'green' },
|
||||
imagedel: { icon: 'tg-image-minus', color: 'red' },
|
||||
spellcheck: { icon: 'spell-check', color: 'green' },
|
||||
map: { icon: 'map', color: 'green' },
|
||||
experiment: { icon: 'radiation', color: 'yellow' },
|
||||
balance: { icon: 'balance-scale-right', color: 'yellow' },
|
||||
code_imp: { icon: 'code', color: 'green' },
|
||||
refactor: { icon: 'tools', color: 'green' },
|
||||
config: { icon: 'cogs', color: 'purple' },
|
||||
admin: { icon: 'user-shield', color: 'purple' },
|
||||
server: { icon: 'server', color: 'purple' },
|
||||
tgs: { icon: 'toolbox', color: 'purple' },
|
||||
tweak: { icon: 'wrench', color: 'green' },
|
||||
unknown: { icon: 'info-circle', color: 'label' },
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import dateformat from 'dateformat';
|
||||
import yaml from 'js-yaml';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { resolveAsset } from 'tgui/assets';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
import { Changes } from './Changes';
|
||||
import { DateDropdown } from './DateDropdown';
|
||||
import { Footer } from './Resources/Footer';
|
||||
import { Header } from './Resources/Header';
|
||||
import { Testmerges } from './Testmerges';
|
||||
import type { ChangelogData, ChangelogEntry } from './types';
|
||||
|
||||
export const Changelog = (props) => {
|
||||
const {
|
||||
act,
|
||||
data: { dates, testmerges },
|
||||
} = useBackend<ChangelogData>();
|
||||
const [selectedDate, setSelectedDate] = useState('');
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [data, setData] = useState<string | ChangelogEntry>(
|
||||
'Loading changelog data...',
|
||||
);
|
||||
|
||||
const sortedDates = useMemo(() => dates.toSorted().toReversed(), [dates]);
|
||||
const dateChoices: string[] = useMemo(
|
||||
() => sortedDates.map((date) => dateformat(date, 'mmmm yyyy', true)),
|
||||
[sortedDates],
|
||||
);
|
||||
|
||||
function getData(date: string, attemptNumber = 1) {
|
||||
const maxAttempts = 6;
|
||||
|
||||
if (attemptNumber > maxAttempts) {
|
||||
return setData(`Failed to load data after ${maxAttempts} attempts`);
|
||||
}
|
||||
|
||||
act('get_month', { date });
|
||||
|
||||
fetch(resolveAsset(`${date}.yml`)).then(async (changelogData) => {
|
||||
const result = await changelogData.text();
|
||||
const errorRegex = /^Cannot find/;
|
||||
|
||||
if (errorRegex.test(result)) {
|
||||
const timeout = 50 + attemptNumber * 50;
|
||||
|
||||
setData(`Loading changelog data${'.'.repeat(attemptNumber + 3)}`);
|
||||
setTimeout(() => {
|
||||
getData(date, attemptNumber + 1);
|
||||
}, timeout);
|
||||
} else {
|
||||
const parsed = yaml.load(result, { schema: yaml.CORE_SCHEMA });
|
||||
if (parsed === null || parsed === undefined) {
|
||||
setData('Changelog is empty or invalid');
|
||||
} else if (typeof parsed === 'string' || typeof parsed === 'object') {
|
||||
setData(parsed);
|
||||
} else {
|
||||
setData('Unexpected changelog format');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDate(dateChoices[0]);
|
||||
getData(sortedDates[0]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Window
|
||||
title="Changelog"
|
||||
width={testmerges.length > 0 ? 1000 : 675}
|
||||
height={650}
|
||||
>
|
||||
<Window.Content scrollable>
|
||||
<Header
|
||||
dateDropdown={
|
||||
<DateDropdown
|
||||
selectedIndex={selectedIndex}
|
||||
setData={setData}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
dateChoices={dateChoices}
|
||||
sortedDates={sortedDates}
|
||||
getData={getData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack>
|
||||
<Stack.Item grow>
|
||||
<Changes data={data} />
|
||||
</Stack.Item>
|
||||
{testmerges.length > 0 && (
|
||||
<Stack.Item width="50%">
|
||||
<Testmerges />
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
{typeof data === 'string' && <p>{data}</p>}
|
||||
<Footer
|
||||
dateDropdown={
|
||||
<DateDropdown
|
||||
selectedIndex={selectedIndex}
|
||||
setData={setData}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
selectedDate={selectedDate}
|
||||
setSelectedDate={setSelectedDate}
|
||||
dateChoices={dateChoices}
|
||||
sortedDates={sortedDates}
|
||||
getData={getData}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export type ChangelogEntry = Record<string, Record<string, ChangeEntry[]>>;
|
||||
type ChangeEntry = Record<string, string>;
|
||||
|
||||
export type Testmerge = {
|
||||
title: string;
|
||||
number: number;
|
||||
link: string;
|
||||
author: string;
|
||||
changes: Record<string, string[]>;
|
||||
};
|
||||
|
||||
export type ChangelogData = {
|
||||
discord_url?: string;
|
||||
dates: string[];
|
||||
testmerges: Testmerge[];
|
||||
};
|
||||
Reference in New Issue
Block a user