Cache forms #187

Merged
enes merged 9 commits from 166-caching-fields into staging 2025-01-09 13:45:45 +00:00
13 changed files with 282 additions and 167 deletions

View File

@ -1,13 +1,19 @@
[ [
{ "name": "gameplay ", "sub": ["difficulty"]}, { "name": "gameplay ", "sub": ["difficulty"] },
{ "name": "input", "sub": ["key mapping", "macro"]}, { "name": "input", "sub": ["key mapping", "macro"] },
{ "name": "visual", "sub": ["textures", "lighting", "character models", "environment models"] }, {
"name": "visual",
"sub": ["textures", "lighting", "character models", "environment models"]
},
{ "name": "audio", "sub": ["sfx", "music", "voice"] }, { "name": "audio", "sub": ["sfx", "music", "voice"] },
{ "name": "user interface", "sub": ["hud", "menu"] }, { "name": "user interface", "sub": ["hud", "menu"] },
{ "name": "quality of life", "sub": ["bug fixes", "performance", "accessibility"] }, {
"name": "quality of life",
"sub": ["bug fixes", "performance", "accessibility"]
},
"total conversions", "total conversions",
"translation", "translation",
"multiplayer", "multiplayer",
"clothing", "clothing",
"Mod Manager" "mod manager"
] ]

View File

@ -22,7 +22,7 @@ import {
ModFormState, ModFormState,
ModPageLoaderResult ModPageLoaderResult
} from '../types' } from '../types'
import { initializeFormState } from '../utils' import { initializeFormState, MOD_DRAFT_CACHE_KEY } from '../utils'
import { CheckboxField, InputField, InputFieldWithImageUpload } from './Inputs' import { CheckboxField, InputField, InputFieldWithImageUpload } from './Inputs'
import { OriginalAuthor } from './OriginalAuthor' import { OriginalAuthor } from './OriginalAuthor'
import { CategoryAutocomplete } from './CategoryAutocomplete' import { CategoryAutocomplete } from './CategoryAutocomplete'
@ -31,6 +31,7 @@ import { Editor, EditorRef } from './Markdown/Editor'
import { MEDIA_OPTIONS } from 'controllers' import { MEDIA_OPTIONS } from 'controllers'
import { InputError } from './Inputs/Error' import { InputError } from './Inputs/Error'
import { ImageUpload } from './Inputs/ImageUpload' import { ImageUpload } from './Inputs/ImageUpload'
import { useLocalCache } from 'hooks/useLocalCache'
interface GameOption { interface GameOption {
value: string value: string
@ -45,9 +46,19 @@ export const ModForm = () => {
const submit = useSubmit() const submit = useSubmit()
const games = useGames() const games = useGames()
const [gameOptions, setGameOptions] = useState<GameOption[]>([]) const [gameOptions, setGameOptions] = useState<GameOption[]>([])
// Enable cache for the new mod
const isEditing = typeof mod !== 'undefined'
const [cache, setCache, clearCache] =
useLocalCache<ModFormState>(MOD_DRAFT_CACHE_KEY)
const [formState, setFormState] = useState<ModFormState>( const [formState, setFormState] = useState<ModFormState>(
initializeFormState(mod) isEditing ? initializeFormState(mod) : cache ? cache : initializeFormState()
) )
useEffect(() => {
!isEditing && setCache(formState)
}, [formState, isEditing, setCache])
const editorRef = useRef<EditorRef>(null) const editorRef = useRef<EditorRef>(null)
useEffect(() => { useEffect(() => {
@ -145,45 +156,42 @@ export const ModForm = () => {
) )
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false) const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
const handleReset = () => { const handleReset = useCallback(() => {
setShowConfirmPopup(true) setShowConfirmPopup(true)
} }, [])
const handleResetConfirm = (confirm: boolean) => { const handleResetConfirm = useCallback(
setShowConfirmPopup(false) (confirm: boolean) => {
setShowConfirmPopup(false)
// Cancel if not confirmed // Cancel if not confirmed
if (!confirm) return if (!confirm) return
// Editing // Reset fields to the initial or original existing data
if (mod) { const initialState = initializeFormState(mod)
const initial = initializeFormState(mod)
// Reset editor // Reset editor
editorRef.current?.setMarkdown(initial.body) editorRef.current?.setMarkdown(initialState.body)
setFormState(initialState)
// Reset fields to the original existing data // Clear cache
setFormState(initial) !isEditing && clearCache()
return },
} [clearCache, isEditing, mod]
)
// New - set form state to the initial (clear form state) const handlePublish = useCallback(
setFormState(initializeFormState()) (e: React.FormEvent<HTMLFormElement>) => {
} e.preventDefault()
const handlePublish = () => { submit(JSON.stringify(formState), {
submit(JSON.stringify(formState), { method: isEditing ? 'put' : 'post',
method: mod ? 'put' : 'post', encType: 'application/json'
encType: 'application/json' })
}) },
} [formState, isEditing, submit]
)
return ( return (
<form <form className='IBMSMSMBS_Write' onSubmit={handlePublish}>
className='IBMSMSMBS_Write'
onSubmit={(e) => {
e.preventDefault()
handlePublish()
}}
>
<GameDropdown <GameDropdown
options={gameOptions} options={gameOptions}
selected={formState?.game} selected={formState?.game}
@ -406,7 +414,7 @@ export const ModForm = () => {
navigation.state === 'loading' || navigation.state === 'submitting' navigation.state === 'loading' || navigation.state === 'submitting'
} }
> >
{mod ? 'Reset' : 'Clear fields'} {isEditing ? 'Reset' : 'Clear fields'}
</button> </button>
<button <button
className='btn btnMain' className='btn btnMain'

View File

@ -9,3 +9,4 @@ export * from './useNDKContext'
export * from './useScrollDisable' export * from './useScrollDisable'
export * from './useLocalStorage' export * from './useLocalStorage'
export * from './useSessionStorage' export * from './useSessionStorage'
export * from './useLocalCache'

View File

@ -0,0 +1,37 @@
import { useCallback, useEffect, useState } from 'react'
import { setLocalStorageItem, removeLocalStorageItem } from 'utils'
export function useLocalCache<T>(
key: string
): [
T | undefined,
React.Dispatch<React.SetStateAction<T | undefined>>,
() => void
] {
const [cache, setCache] = useState<T | undefined>(() => {
const storedValue = window.localStorage.getItem(key)
if (storedValue === null) return undefined
// Parse the value
const parsedStoredValue = JSON.parse(storedValue)
return parsedStoredValue
})
useEffect(() => {
try {
if (cache) {
setLocalStorageItem(key, JSON.stringify(cache))
} else {
removeLocalStorageItem(key)
}
} catch (e) {
console.warn(e)
}
}, [cache, key])
const clearCache = useCallback(() => {
setCache(undefined)
}, [])
return [cache, setCache, clearCache]
}

View File

@ -1,6 +1,7 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { import {
getLocalStorageItem, getLocalStorageItem,
mergeWithInitialValue,
removeLocalStorageItem, removeLocalStorageItem,
setLocalStorageItem setLocalStorageItem
} from 'utils' } from 'utils'
@ -10,17 +11,6 @@ const useLocalStorageSubscribe = (callback: () => void) => {
return () => window.removeEventListener('storage', callback) return () => window.removeEventListener('storage', callback)
} }
function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
if (
!Array.isArray(storedValue) &&
typeof storedValue === 'object' &&
storedValue !== null
) {
return { ...initialValue, ...storedValue }
}
return storedValue
}
export function useLocalStorage<T>( export function useLocalStorage<T>(
key: string, key: string,
initialValue: T initialValue: T

View File

@ -1,6 +1,7 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { import {
getSessionStorageItem, getSessionStorageItem,
mergeWithInitialValue,
removeSessionStorageItem, removeSessionStorageItem,
setSessionStorageItem setSessionStorageItem
} from 'utils' } from 'utils'
@ -10,17 +11,6 @@ const useSessionStorageSubscribe = (callback: () => void) => {
return () => window.removeEventListener('sessionStorage', callback) return () => window.removeEventListener('sessionStorage', callback)
} }
function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
if (
!Array.isArray(storedValue) &&
typeof storedValue === 'object' &&
storedValue !== null
) {
return { ...initialValue, ...storedValue }
}
return storedValue
}
export function useSessionStorage<T>( export function useSessionStorage<T>(
key: string, key: string,
initialValue: T initialValue: T

View File

@ -12,7 +12,9 @@ import {
isValidUrl, isValidUrl,
log, log,
LogType, LogType,
now MOD_DRAFT_CACHE_KEY,
now,
removeLocalStorageItem
} from 'utils' } from 'utils'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { T_TAG_VALUE } from '../../constants' import { T_TAG_VALUE } from '../../constants'
@ -141,6 +143,8 @@ export const submitModRouteAction =
)}` )}`
) )
!isEditing && removeLocalStorageItem(MOD_DRAFT_CACHE_KEY)
const naddr = nip19.naddrEncode({ const naddr = nip19.naddrEncode({
identifier: aTag, identifier: aTag,
pubkey: signedEvent.pubkey, pubkey: signedEvent.pubkey,
@ -209,13 +213,6 @@ const validateState = async (
} }
} }
if (
formState.repost &&
(!formState.originalAuthor || formState.originalAuthor === '')
) {
errors.originalAuthor = 'Original author field can not be empty'
}
if (!formState.tags || formState.tags === '') { if (!formState.tags || formState.tags === '') {
errors.tags = 'Tags field can not be empty' errors.tags = 'Tags field can not be empty'
} }

View File

@ -3,12 +3,13 @@ import { ActionFunctionArgs, redirect } from 'react-router-dom'
import { getBlogPageRoute } from 'routes' import { getBlogPageRoute } from 'routes'
import { BlogFormErrors, BlogEventSubmitForm, BlogEventEditForm } from 'types' import { BlogFormErrors, BlogEventSubmitForm, BlogEventEditForm } from 'types'
import { import {
BLOG_DRAFT_CACHE_KEY,
isReachable, isReachable,
isValidImageUrl, isValidImageUrl,
log, log,
LogType, LogType,
now, now,
parseFormData removeLocalStorageItem
} from 'utils' } from 'utils'
import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools' import { kinds, UnsignedEvent, Event, nip19 } from 'nostr-tools'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@ -43,12 +44,9 @@ export const writeRouteAction =
} }
// Get the form data from submit request // Get the form data from submit request
const formData = await request.formData() const formSubmit = (await request.json()) as
| BlogEventSubmitForm
// Parse the the data | BlogEventEditForm
const formSubmit = parseFormData<BlogEventSubmitForm | BlogEventEditForm>(
formData
)
// Check for errors // Check for errors
const formErrors = await validateFormData(formSubmit) const formErrors = await validateFormData(formSubmit)
@ -80,7 +78,7 @@ export const writeRouteAction =
const tTags = formSubmit const tTags = formSubmit
.tags!.toLowerCase() .tags!.toLowerCase()
.split(',') .split(',')
.map((t) => ['t', t]) .map((t) => ['t', t.trim()])
const tags = [ const tags = [
['d', uuid], ['d', uuid],
@ -95,7 +93,7 @@ export const writeRouteAction =
// Add NSFW tag, L label namespace standardized tag // Add NSFW tag, L label namespace standardized tag
// https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags // https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags
if (formSubmit.nsfw === 'on') tags.push(['L', 'content-warning']) if (formSubmit.nsfw) tags.push(['L', 'content-warning'])
const unsignedEvent: UnsignedEvent = { const unsignedEvent: UnsignedEvent = {
kind: kinds.LongFormArticle, kind: kinds.LongFormArticle,
@ -128,6 +126,9 @@ export const writeRouteAction =
'\n' '\n'
)}` )}`
) )
!isEditing && removeLocalStorageItem(BLOG_DRAFT_CACHE_KEY)
const naddr = nip19.naddrEncode({ const naddr = nip19.naddrEncode({
identifier: uuid, identifier: uuid,
pubkey: signedEvent.pubkey, pubkey: signedEvent.pubkey,

View File

@ -1,60 +1,123 @@
import { useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { import {
Form,
useActionData, useActionData,
useLoaderData, useLoaderData,
useNavigation useNavigation,
useSubmit
} from 'react-router-dom' } from 'react-router-dom'
import { import {
CheckboxFieldUncontrolled, CheckboxField,
InputFieldUncontrolled, InputField,
InputFieldWithImageUpload InputFieldWithImageUpload
} from '../../components/Inputs' } from 'components/Inputs'
import { ProfileSection } from '../../components/ProfileSection' import { ProfileSection } from 'components/ProfileSection'
import { useAppSelector } from '../../hooks' import { useAppSelector, useLocalCache } from 'hooks'
import { BlogFormErrors, BlogPageLoaderResult } from 'types' import {
import '../../styles/innerPage.css' BlogEventEditForm,
import '../../styles/styles.css' BlogEventSubmitForm,
import '../../styles/write.css' BlogFormErrors,
BlogPageLoaderResult
} from 'types'
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { AlertPopup } from 'components/AlertPopup' import { AlertPopup } from 'components/AlertPopup'
import { Editor, EditorRef } from 'components/Markdown/Editor' import { Editor, EditorRef } from 'components/Markdown/Editor'
import { InputError } from 'components/Inputs/Error' import { InputError } from 'components/Inputs/Error'
import { BLOG_DRAFT_CACHE_KEY, initializeBlogForm } from 'utils'
import 'styles/innerPage.css'
import 'styles/styles.css'
import 'styles/write.css'
export const WritePage = () => { export const WritePage = () => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const data = useLoaderData() as BlogPageLoaderResult const data = useLoaderData() as BlogPageLoaderResult
const formErrors = useActionData() as BlogFormErrors const formErrors = useActionData() as BlogFormErrors
const navigation = useNavigation() const navigation = useNavigation()
const submit = useSubmit()
const blog = data?.blog const blog = data?.blog
const title = data?.blog ? 'Edit blog post' : 'Submit a blog post'
const [content, setContent] = useState(blog?.content || '')
const [image, setImage] = useState(blog?.image || '')
const formRef = useRef<HTMLFormElement>(null) // Enable cache for the new blog
const isEditing = typeof data?.blog !== 'undefined'
const [cache, setCache, clearCache] =
useLocalCache<BlogEventSubmitForm>(BLOG_DRAFT_CACHE_KEY)
const title = isEditing ? 'Edit blog post' : 'Submit a blog post'
const [formState, setFormState] = useState<
BlogEventSubmitForm | BlogEventEditForm
>(isEditing ? initializeBlogForm(blog) : cache ? cache : initializeBlogForm())
useEffect(() => {
!isEditing && setCache(formState)
}, [formState, isEditing, setCache])
const editorRef = useRef<EditorRef>(null) const editorRef = useRef<EditorRef>(null)
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false) const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
const handleReset = () => { const handleReset = useCallback(() => {
setShowConfirmPopup(true) setShowConfirmPopup(true)
} }, [])
const handleResetConfirm = (confirm: boolean) => { const handleResetConfirm = useCallback(
setShowConfirmPopup(false) (confirm: boolean) => {
setShowConfirmPopup(false)
// Cancel if not confirmed // Cancel if not confirmed
if (!confirm) return if (!confirm) return
// Reset featured image const initialState = initializeBlogForm(blog)
setImage(blog?.image || '')
// Reset editor // Reset editor
if (blog?.content) { editorRef.current?.setMarkdown(initialState.content)
editorRef.current?.setMarkdown(blog?.content) setFormState(initialState)
}
formRef.current?.reset() // Clear cache
} !isEditing && clearCache()
},
[blog, clearCache, isEditing]
)
const handleImageChange = useCallback((_name: string, value: string) => {
setFormState((prev) => ({
...prev,
image: value
}))
}, [])
const handleInputChange = useCallback((name: string, value: string) => {
setFormState((prevState) => ({
...prevState,
[name]: value
}))
}, [])
const handleEditorChange = useCallback(
(md: string) => {
handleInputChange('content', md)
},
[handleInputChange]
)
const handleCheckboxChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target
setFormState((prevState) => ({
...prevState,
[name]: checked
}))
},
[]
)
const handleFormSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault()
submit(JSON.stringify(formState), {
method: isEditing ? 'put' : 'post',
encType: 'application/json'
})
},
[formState, isEditing, submit]
)
return ( return (
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
@ -71,82 +134,63 @@ export const WritePage = () => {
{navigation.state === 'submitting' && ( {navigation.state === 'submitting' && (
<LoadingSpinner desc='Publishing blog to relays' /> <LoadingSpinner desc='Publishing blog to relays' />
)} )}
<Form <form className='IBMSMSMBS_Write' onSubmit={handleFormSubmit}>
ref={formRef} <InputField
className='IBMSMSMBS_Write'
method={blog ? 'put' : 'post'}
>
<InputFieldUncontrolled
label='Title' label='Title'
name='title' name='title'
defaultValue={blog?.title} value={formState.title}
error={formErrors?.title} error={formErrors?.title}
onChange={handleInputChange}
placeholder='Blog title'
/> />
<div className='inputLabelWrapperMain'> <div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Content</label> <label className='form-label labelMain'>Content</label>
<div className='inputMain'> <div className='inputMain'>
<Editor <Editor
ref={editorRef} ref={editorRef}
markdown={content} markdown={formState.content}
onChange={(md) => { onChange={handleEditorChange}
setContent(md)
}}
/> />
</div> </div>
{typeof formErrors?.content !== 'undefined' && ( {typeof formErrors?.content !== 'undefined' && (
<InputError message={formErrors?.content} /> <InputError message={formErrors?.content} />
)} )}
{/* encode to keep the markdown formatting */}
<input
name='content'
hidden
value={encodeURIComponent(content)}
readOnly
/>
</div> </div>
<InputFieldWithImageUpload <InputFieldWithImageUpload
label='Featured Image URL' label='Featured Image URL'
name='image' name='image'
inputMode='url' inputMode='url'
value={image} value={formState.image}
error={formErrors?.image} error={formErrors?.image}
onInputChange={(_, value) => setImage(value)} onInputChange={handleImageChange}
placeholder='Image URL' placeholder='Image URL'
/> />
<InputFieldUncontrolled <InputField
label='Summary' label='Summary'
name='summary' name='summary'
type='textarea' type='textarea'
defaultValue={blog?.summary} value={formState.summary}
error={formErrors?.summary} error={formErrors?.summary}
onChange={handleInputChange}
placeholder={'This is a quick description of my blog'}
/> />
<InputFieldUncontrolled <InputField
label='Tags' label='Tags'
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)' description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
placeholder='Tags' placeholder='Tags'
name='tags' name='tags'
defaultValue={blog?.tTags?.join(', ')} value={formState.tags}
error={formErrors?.tags} error={formErrors?.tags}
onChange={handleInputChange}
/> />
<CheckboxFieldUncontrolled <CheckboxField
label='This post is not safe for work (NSFW)' label='This post is not safe for work (NSFW)'
name='nsfw' name='nsfw'
defaultChecked={blog?.nsfw} isChecked={formState.nsfw}
handleChange={handleCheckboxChange}
type='stylized'
/> />
{typeof blog?.dTag !== 'undefined' && (
<input name='dTag' hidden value={blog.dTag} readOnly />
)}
{typeof blog?.rTag !== 'undefined' && (
<input name='rTag' hidden value={blog.rTag} readOnly />
)}
{typeof blog?.published_at !== 'undefined' && (
<input
name='published_at'
hidden
value={blog.published_at}
readOnly
/>
)}
<div className='IBMSMSMBS_WriteAction'> <div className='IBMSMSMBS_WriteAction'>
<button <button
className='btn btnMain' className='btn btnMain'
@ -157,7 +201,7 @@ export const WritePage = () => {
navigation.state === 'submitting' navigation.state === 'submitting'
} }
> >
{blog ? 'Reset' : 'Clear fields'} {isEditing ? 'Reset' : 'Clear fields'}
</button> </button>
<button <button
className='btn btnMain' className='btn btnMain'
@ -178,13 +222,13 @@ export const WritePage = () => {
handleClose={() => setShowConfirmPopup(false)} handleClose={() => setShowConfirmPopup(false)}
header={'Are you sure?'} header={'Are you sure?'}
label={ label={
blog isEditing
? `Are you sure you want to clear all changes?` ? `Are you sure you want to clear all changes?`
: `Are you sure you want to clear all field data?` : `Are you sure you want to clear all field data?`
} }
/> />
)} )}
</Form> </form>
</div> </div>
{userState.auth && userState.user?.pubkey && ( {userState.auth && userState.user?.pubkey && (
<ProfileSection pubkey={userState.user.pubkey as string} /> <ProfileSection pubkey={userState.user.pubkey as string} />

View File

@ -20,9 +20,7 @@ export interface BlogDetails extends BlogForm {
tTags: string[] tTags: string[]
} }
export interface BlogEventSubmitForm extends Omit<BlogForm, 'nsfw'> { export interface BlogEventSubmitForm extends BlogForm {}
nsfw: string
}
export interface BlogEventEditForm extends BlogEventSubmitForm { export interface BlogEventEditForm extends BlogEventSubmitForm {
dTag: string dTag: string

View File

@ -1,5 +1,10 @@
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk'
import { BlogCardDetails, BlogDetails } from 'types' import {
BlogCardDetails,
BlogDetails,
BlogEventEditForm,
BlogEventSubmitForm
} from 'types'
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr' import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
import { kinds, nip19 } from 'nostr-tools' import { kinds, nip19 } from 'nostr-tools'
@ -50,3 +55,25 @@ export const extractBlogCardDetails = (
: undefined : undefined
} }
} }
export const initializeBlogForm = (
blog?: Partial<BlogDetails>
): BlogEventSubmitForm | BlogEventEditForm => ({
content: blog?.content || '',
image: blog?.image || '',
nsfw: blog?.nsfw || false,
summary: blog?.summary || '',
title: blog?.title || '',
tags: blog?.tTags?.join(', ') || '',
...(blog?.aTag && {
aTag: blog.aTag
}),
...(blog?.dTag && {
dTag: blog.dTag
}),
...(blog?.published_at && {
published_at: blog.published_at
})
})
export const BLOG_DRAFT_CACHE_KEY = 'draft-blog'

View File

@ -1,3 +1,4 @@
import _ from 'lodash'
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { ModDetails, ModFormState } from '../types' import { ModDetails, ModFormState } from '../types'
@ -131,16 +132,20 @@ export const initializeFormState = (
originalAuthor: existingModData?.originalAuthor || undefined, originalAuthor: existingModData?.originalAuthor || undefined,
screenshotsUrls: existingModData?.screenshotsUrls || [''], screenshotsUrls: existingModData?.screenshotsUrls || [''],
tags: existingModData?.tags.join(',') || '', tags: existingModData?.tags.join(',') || '',
lTags: existingModData?.lTags || [], lTags: existingModData ? _.clone(existingModData.lTags) : [],
LTags: existingModData?.LTags || [], LTags: existingModData ? _.clone(existingModData.lTags) : [],
downloadUrls: existingModData?.downloadUrls || [ downloadUrls: existingModData
{ ? _.cloneDeep(existingModData.downloadUrls)
url: '', : [
hash: '', {
signatureKey: '', url: '',
malwareScanLink: '', hash: '',
modVersion: '', signatureKey: '',
customNote: '' malwareScanLink: '',
} modVersion: '',
] customNote: ''
}
]
}) })
export const MOD_DRAFT_CACHE_KEY = 'draft-mod'

View File

@ -180,3 +180,14 @@ export const getFallbackPubkey = () => {
// Silently ignore // Silently ignore
} }
} }
export function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
if (
!Array.isArray(storedValue) &&
typeof storedValue === 'object' &&
storedValue !== null
) {
return { ...initialValue, ...storedValue }
}
return storedValue
}