11<template >
2- <div class =" relative h-full w-full overflow-y-auto" >
2+ <div class =" relative h-full w-full select-none overflow-y-auto" >
33 <div v-if =" propsData" class =" flex h-full w-full flex-col justify-between gap-6 overflow-y-auto" >
44 <div class =" card flex flex-col gap-4" >
5- <label for = " username-field " class =" flex flex-col gap-2" >
6- <span class =" text-lg font-bold text-contrast" >Server Properties</span >
7- <span > Edit the Minecraft server properties file.</span >
8- </label >
5+ <div class =" flex flex-col gap-2" >
6+ <h2 class =" m-0 text-lg font-bold text-contrast" >Server Properties</h2 >
7+ <p class = " m-0 " > Edit the Minecraft server properties file.</p >
8+ </div >
99 <div class =" flex flex-col gap-4 rounded-xl bg-table-alternateRow p-4" >
1010 <div class =" relative w-full text-sm" >
11- <label class = " sr-only " for = " search " >Search</label >
11+ <label for = " search-server-properties " class = " sr-only " >Search server properties </label >
1212 <SearchIcon
1313 class =" pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2"
1414 aria-hidden =" true"
1515 />
1616 <input
17- id =" search"
17+ id =" search-server-properties "
1818 v-model =" searchInput"
1919 class =" w-full pl-9"
2020 type =" search"
2626 <div
2727 v-for =" (property, index) in filteredProperties"
2828 :key =" index"
29- class =" mb-2 mt-2 flex items-center justify-between pb -2"
29+ class =" flex items-center justify-between py -2"
3030 >
31- <label :for =" index.toString()" class =" flex items-center" >
32- {{
33- index
34- .toString()
35- .split(/[-.]/)
36- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
37- .join(" ")
38- }}
31+ <div class =" flex items-center" >
32+ <span :id =" `property-label-${index}`" >{{ formatPropertyName(index) }}</span >
3933 <span v-if =" overrides[index] && overrides[index].info" class =" ml-2" >
4034 <EyeIcon v-tooltip =" overrides[index].info" />
4135 </span >
42- </label >
43- <div v-if =" overrides[index] && overrides[index].type === 'dropdown'" >
36+ </div >
37+ <div
38+ v-if =" overrides[index] && overrides[index].type === 'dropdown'"
39+ class =" flex w-[320px] justify-end"
40+ >
4441 <DropdownSelect
42+ :id =" `server-property-${index}`"
4543 v-model =" liveProperties[index]"
46- :name ="
47- index
48- .toString()
49- .split('-')
50- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
51- .join(' ')
52- "
44+ :name =" formatPropertyName(index)"
5345 :options =" overrides[index].options"
46+ :aria-labelledby =" `property-label-${index}`"
5447 placeholder =" Select..."
5548 />
5649 </div >
57- <div v-else-if =" typeof property === 'boolean'" >
50+ <div v-else-if =" typeof property === 'boolean'" class = " flex w-[320px] justify-end " >
5851 <input
59- id =" property.id "
52+ : id =" `server- property-${index}` "
6053 v-model =" liveProperties[index]"
6154 class =" switch stylized-toggle"
6255 type =" checkbox"
56+ :aria-labelledby =" `property-label-${index}`"
6357 />
6458 </div >
6559 <div v-else-if =" typeof property === 'number'" class =" w-[320px]" >
6660 <input
67- :id =" index.toString() "
61+ :id =" `server-property-${ index}` "
6862 v-model.number =" liveProperties[index]"
6963 type =" number"
7064 class =" w-full border p-2"
65+ :aria-labelledby =" `property-label-${index}`"
7166 />
7267 </div >
73- <div
74- v-else-if ="
75- typeof property === 'object' ||
76- property.includes(',') ||
77- property.includes('{') ||
78- property.includes('}') ||
79- property.includes('[') ||
80- property.includes(']') ||
81- property.length > 30
82- "
83- class =" w-[320px]"
84- >
68+ <div v-else-if =" isComplexProperty(property)" class =" w-[320px]" >
8569 <textarea
86- :id =" index.toString() "
87- :value = " JSON.stringify(property, null, 2) "
70+ :id =" `server-property-${ index}` "
71+ v-model = " liveProperties[index] "
8872 class =" w-full resize-y rounded-xl border p-2"
73+ :aria-labelledby =" `property-label-${index}`"
8974 ></textarea >
9075 </div >
91- <div v-else class =" w-[320px]" >
76+ <div v-else class =" flex w-[320px] justify-end " >
9277 <input
93- :id =" index.toString() "
94- :value = " property "
78+ :id =" `server-property-${ index}` "
79+ v-model = " liveProperties[index] "
9580 type =" text"
9681 class =" w-full rounded-xl border p-2"
82+ :aria-labelledby =" `property-label-${index}`"
9783 />
9884 </div >
9985 </div >
11298 <UiServersPyroLoading v-else />
11399 <div class =" absolute bottom-[2.5%] left-[2.5%] z-10 w-[95%]" >
114100 <UiServersSaveBanner
115- :is-visible =" !! hasUnsavedChanges"
101+ :is-visible =" hasUnsavedChanges"
116102 :server =" props.server"
117103 :is-updating =" isUpdating"
118104 restart
124110</template >
125111
126112<script setup lang="ts">
127- import { ref , watch } from " vue" ;
113+ import { ref , watch , computed } from " vue" ;
128114import { DropdownSelect } from " @modrinth/ui" ;
129115import { EyeIcon , SearchIcon } from " @modrinth/assets" ;
130116import Fuse from " fuse.js" ;
@@ -138,9 +124,6 @@ const tags = useTags();
138124
139125const isUpdating = ref (false );
140126
141- const changedPropertiesState = ref ({});
142- const hasUnsavedChanges = computed (() => JSON .stringify (changedPropertiesState .value ) !== " {}" );
143-
144127const searchInput = ref (" " );
145128
146129const data = computed (() => props .server .general );
@@ -149,6 +132,27 @@ const { data: propsData } = await useAsyncData(
149132 async () => await props .server .general ?.fetchConfigFile (" ServerProperties" ),
150133);
151134
135+ const liveProperties = ref <Record <string , any >>({});
136+ const originalProperties = ref <Record <string , any >>({});
137+
138+ watch (
139+ propsData ,
140+ (newPropsData ) => {
141+ if (newPropsData ) {
142+ liveProperties .value = JSON .parse (JSON .stringify (newPropsData ));
143+ originalProperties .value = JSON .parse (JSON .stringify (newPropsData ));
144+ }
145+ },
146+ { immediate: true },
147+ );
148+
149+ const hasUnsavedChanges = computed (() => {
150+ return Object .keys (liveProperties .value ).some (
151+ (key ) =>
152+ JSON .stringify (liveProperties .value [key ]) !== JSON .stringify (originalProperties .value [key ]),
153+ );
154+ });
155+
152156const getDifficultyOptions = () => {
153157 const pre113Versions = tags .value .gameVersions
154158 .filter ((v ) => {
@@ -174,8 +178,6 @@ const overrides: { [key: string]: { type: string; options?: string[]; info?: str
174178 },
175179};
176180
177- const liveProperties = ref (JSON .parse (JSON .stringify (propsData .value )));
178-
179181const fuse = computed (() => {
180182 if (! liveProperties .value ) return null ;
181183
@@ -200,26 +202,6 @@ const filteredProperties = computed(() => {
200202 return Object .fromEntries (results .map (({ item }) => [item .key , liveProperties .value [item .key ]]));
201203});
202204
203- watch (
204- liveProperties ,
205- (newProperties ) => {
206- changedPropertiesState .value = {};
207- const changed = [];
208- for (const key in newProperties ) {
209- // @ts-ignore https://typescript.tv/errors/#ts7053
210- if (newProperties [key ] !== data .value [key ]) {
211- changed .push (key );
212- }
213- }
214- // @ts-ignore
215- for (const key of changed ) {
216- // @ts-ignore
217- changedPropertiesState .value [key ] = newProperties [key ];
218- }
219- },
220- { deep: true },
221- );
222-
223205const constructServerProperties = (): string => {
224206 const properties = liveProperties .value ;
225207
@@ -243,7 +225,7 @@ const saveProperties = async () => {
243225 isUpdating .value = true ;
244226 await props .server .fs ?.updateFile (" server.properties" , constructServerProperties ());
245227 await new Promise ((resolve ) => setTimeout (resolve , 500 ));
246- changedPropertiesState .value = {} ;
228+ originalProperties .value = JSON . parse ( JSON . stringify ( liveProperties . value )) ;
247229 await props .server .refresh ();
248230 addNotification ({
249231 group: " serverOptions" ,
@@ -265,8 +247,27 @@ const saveProperties = async () => {
265247};
266248
267249const resetProperties = async () => {
268- liveProperties .value = JSON .parse (JSON .stringify (propsData .value ));
250+ liveProperties .value = JSON .parse (JSON .stringify (originalProperties .value ));
269251 await new Promise ((resolve ) => setTimeout (resolve , 200 ));
270- changedPropertiesState .value = {};
252+ };
253+
254+ const formatPropertyName = (propertyName : string ): string => {
255+ return propertyName
256+ .split (/ [-. ] / )
257+ .map ((word ) => word .charAt (0 ).toUpperCase () + word .slice (1 ))
258+ .join (" " );
259+ };
260+
261+ const isComplexProperty = (property : any ): boolean => {
262+ return (
263+ typeof property === " object" ||
264+ (typeof property === " string" &&
265+ (property .includes (" ," ) ||
266+ property .includes (" {" ) ||
267+ property .includes (" }" ) ||
268+ property .includes (" [" ) ||
269+ property .includes (" ]" ) ||
270+ property .length > 30 ))
271+ );
271272};
272273 </script >
0 commit comments