mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 23:01:37 +00:00
FIX - make company jobs freely configurable
This commit is contained in:
@@ -232,7 +232,8 @@ Every `Config.*` value from `config.lua`, including server-only sections, and ev
|
||||
file-owned because it decides whether SQL configuration is loaded. Lists, nested objects, vectors,
|
||||
and numeric-keyed Lua tables use structured editors instead of raw JSON. Shipped schema rows stay
|
||||
editable but cannot be renamed, converted, or removed. Every list and table still accepts any number
|
||||
of additional rows; administrator-added rows remain removable.
|
||||
of additional rows; administrator-added rows remain removable. Company job keys are intentionally
|
||||
fully removable because `Config.Companies.Definitions` is a freely managed job collection.
|
||||
|
||||
Media API keys and server peppers are never returned in plaintext to the NUI. Existing secrets are
|
||||
shown only as configured and are replaced only when an administrator enters a new value.
|
||||
@@ -562,8 +563,9 @@ Select `rtx`, `quasar`, `vms`, `rx`, `nolag`, `sn`, `esx_property`, or `qbx_prop
|
||||
|
||||
Company jobs, public profiles, service numbers, services, permissions, locations, and default
|
||||
availability are configured under `Config.Companies.Definitions`. Definitions are not limited to the
|
||||
shipped jobs: add any number of company IDs in the in-game configurator, choose `Table`, and fill the
|
||||
freely configurable `Job` value in the generated full company template.
|
||||
shipped jobs: add any number of company IDs in the in-game configurator and fill the freely
|
||||
configurable `Job` value in the automatically generated full company template. Existing job keys can
|
||||
also be removed; the remaining Companies settings stay available as normal individual fields.
|
||||
|
||||
### Weazel News
|
||||
|
||||
|
||||
@@ -107,6 +107,16 @@ const tableStructure = computed(() =>
|
||||
const mapStructure = computed(() =>
|
||||
props.structure?.kind === 'map' ? props.structure : null,
|
||||
)
|
||||
const canAddTableField = computed(() => {
|
||||
const key = newObjectKey.value.trim()
|
||||
if (
|
||||
!key ||
|
||||
key === '__skyType' ||
|
||||
Object.prototype.hasOwnProperty.call(tableValue.value, key)
|
||||
)
|
||||
return false
|
||||
return !tableStructure.value?.mutableKeys || /^[a-z0-9_-]+$/.test(key)
|
||||
})
|
||||
const tableEntries = computed(() =>
|
||||
Object.entries(tableValue.value).filter(
|
||||
([key]) => key !== '__skyType' && (!mapType.value || key !== 'entries'),
|
||||
@@ -163,6 +173,40 @@ function blankCollectionValue(kind: ValueKind, siblings: unknown[]): unknown {
|
||||
return template === undefined ? blankValue(kind) : blankLike(template)
|
||||
}
|
||||
|
||||
function blankFromStructure(structure: AdminConfiguratorStructure): unknown {
|
||||
if (structure.kind === 'optionalString') return ''
|
||||
if (structure.kind === 'value') return blankValue(structure.valueType)
|
||||
if (structure.kind === 'vector') {
|
||||
const axes = ['x', 'y', 'z', 'w'].slice(
|
||||
0,
|
||||
Number(structure.vectorType.slice(-1)),
|
||||
)
|
||||
return Object.fromEntries([
|
||||
['__skyType', structure.vectorType],
|
||||
...axes.map((axis) => [axis, 0]),
|
||||
])
|
||||
}
|
||||
if (structure.kind === 'list') {
|
||||
return structure.items.map(blankFromStructure)
|
||||
}
|
||||
if (structure.kind === 'map') {
|
||||
return {
|
||||
__skyType: 'map',
|
||||
entries: structure.entries.map((entry) => ({
|
||||
key: entry.key,
|
||||
keyType: entry.keyType,
|
||||
value: blankFromStructure(entry.structure),
|
||||
})),
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(structure.fields).map(([key, field]) => [
|
||||
key,
|
||||
blankFromStructure(field),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function updateScalar(event: Event): void {
|
||||
const target = event.target
|
||||
if (!(target instanceof HTMLInputElement)) return
|
||||
@@ -217,12 +261,30 @@ function updateTableField(key: string, value: unknown): void {
|
||||
emit('update:modelValue', { ...tableValue.value, [key]: value })
|
||||
}
|
||||
|
||||
function removeTableField(key: string): void {
|
||||
if (
|
||||
tableStructure.value &&
|
||||
Object.prototype.hasOwnProperty.call(tableStructure.value.fields, key)
|
||||
function tableFieldStructure(
|
||||
key: string,
|
||||
): AdminConfiguratorStructure | undefined {
|
||||
if (!tableStructure.value) return undefined
|
||||
return (
|
||||
tableStructure.value.fields[key] ??
|
||||
(tableStructure.value.mutableKeys
|
||||
? tableStructure.value.template
|
||||
: undefined)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
function isFixedTableField(key: string): boolean {
|
||||
return (
|
||||
tableStructure.value?.mutableKeys !== true &&
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
tableStructure.value?.fields ?? {},
|
||||
key,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function removeTableField(key: string): void {
|
||||
if (isFixedTableField(key)) return
|
||||
const next = { ...tableValue.value }
|
||||
delete next[key]
|
||||
emit('update:modelValue', next)
|
||||
@@ -230,18 +292,18 @@ function removeTableField(key: string): void {
|
||||
|
||||
function addTableField(): void {
|
||||
const key = newObjectKey.value.trim()
|
||||
if (
|
||||
!key ||
|
||||
key === '__skyType' ||
|
||||
Object.prototype.hasOwnProperty.call(tableValue.value, key)
|
||||
)
|
||||
return
|
||||
if (!canAddTableField.value) return
|
||||
const template = tableStructure.value?.mutableKeys
|
||||
? tableStructure.value.template
|
||||
: undefined
|
||||
emit('update:modelValue', {
|
||||
...tableValue.value,
|
||||
[key]: blankCollectionValue(
|
||||
newObjectKind.value,
|
||||
Object.values(tableValue.value).filter((_, index) => index < 50),
|
||||
),
|
||||
[key]: template
|
||||
? blankFromStructure(template)
|
||||
: blankCollectionValue(
|
||||
newObjectKind.value,
|
||||
Object.values(tableValue.value).filter((_, index) => index < 50),
|
||||
),
|
||||
})
|
||||
newObjectKey.value = ''
|
||||
}
|
||||
@@ -484,7 +546,7 @@ function mapEntryStructure(
|
||||
<strong>{{ key }}</strong>
|
||||
<AdminConfigValueEditor
|
||||
:model-value="value"
|
||||
:structure="tableStructure?.fields[key]"
|
||||
:structure="tableFieldStructure(key)"
|
||||
:aria-label="`${ariaLabel} ${key}`"
|
||||
:labels="labels"
|
||||
:disabled="disabled"
|
||||
@@ -492,13 +554,7 @@ function mapEntryStructure(
|
||||
@update:model-value="updateTableField(key, $event)"
|
||||
/>
|
||||
<button
|
||||
v-if="
|
||||
!vectorType &&
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
tableStructure?.fields ?? {},
|
||||
key,
|
||||
)
|
||||
"
|
||||
v-if="!vectorType && !isFixedTableField(key)"
|
||||
type="button"
|
||||
class="config-structured-editor__remove"
|
||||
:disabled="disabled"
|
||||
@@ -544,6 +600,7 @@ function mapEntryStructure(
|
||||
<form
|
||||
v-else-if="!vectorType"
|
||||
class="config-structured-editor__add-field"
|
||||
:class="{ 'is-mutable-table': tableStructure?.mutableKeys }"
|
||||
@submit.prevent="addTableField"
|
||||
>
|
||||
<input
|
||||
@@ -553,14 +610,18 @@ function mapEntryStructure(
|
||||
:placeholder="labels.keyPlaceholder"
|
||||
:aria-label="labels.keyPlaceholder"
|
||||
/>
|
||||
<select v-model="newObjectKind" :disabled="disabled">
|
||||
<select
|
||||
v-if="!tableStructure?.mutableKeys"
|
||||
v-model="newObjectKind"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<option value="string">{{ labels.types.string }}</option>
|
||||
<option value="number">{{ labels.types.number }}</option>
|
||||
<option value="boolean">{{ labels.types.boolean }}</option>
|
||||
<option value="list">{{ labels.types.list }}</option>
|
||||
<option value="table">{{ labels.types.table }}</option>
|
||||
</select>
|
||||
<button type="submit" :disabled="disabled || !newObjectKey.trim()">
|
||||
<button type="submit" :disabled="disabled || !canAddTableField">
|
||||
<Plus :size="13" />{{ labels.addField }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -797,6 +858,10 @@ function mapEntryStructure(
|
||||
grid-template-columns: 76px minmax(90px, 1fr) 82px auto;
|
||||
}
|
||||
|
||||
.config-structured-editor__add-field.is-mutable-table {
|
||||
grid-template-columns: minmax(100px, 1fr) auto;
|
||||
}
|
||||
|
||||
.config-structured-editor__add-field input {
|
||||
min-width: 0;
|
||||
height: 25px;
|
||||
|
||||
@@ -262,6 +262,9 @@ describe('standalone admin panel contracts', () => {
|
||||
expect(configuratorServer).toContain('validate_structured_value')
|
||||
expect(configuratorServer).toContain('validate_locked_structure')
|
||||
expect(configuratorServer).toContain('build_structure(default_value')
|
||||
expect(configuratorServer).toContain('path == "Companies.Definitions"')
|
||||
expect(configuratorServer).toContain('flatten_company_fields')
|
||||
expect(configuratorServer).toContain('structure.mutableKeys')
|
||||
expect(configuratorServer).toContain('field.type == "stringOrFalse"')
|
||||
expect(configuratorServer).not.toContain('Config.Media = client_payload')
|
||||
expect(configuratorClient).toContain(
|
||||
@@ -280,8 +283,10 @@ describe('standalone admin panel contracts', () => {
|
||||
expect(configuratorValueEditor).toContain('function updateMapKey(')
|
||||
expect(configuratorValueEditor).toContain('mapEntryStructure(current)')
|
||||
expect(configuratorValueEditor).toContain('listStructure?.items[index]')
|
||||
expect(configuratorValueEditor).toContain('tableStructure?.fields[key]')
|
||||
expect(configuratorValueEditor).toContain('function tableFieldStructure(')
|
||||
expect(configuratorValueEditor).toContain('function isFixedTableField(')
|
||||
expect(configuratorValueEditor).toContain('function blankCollectionValue(')
|
||||
expect(configuratorValueEditor).toContain('function blankFromStructure(')
|
||||
expect(configuratorValueEditor).not.toContain('<textarea')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -131,6 +131,8 @@ export type AdminConfiguratorStructure =
|
||||
| {
|
||||
fields: Record<string, AdminConfiguratorStructure>
|
||||
kind: 'table'
|
||||
mutableKeys?: boolean
|
||||
template?: AdminConfiguratorStructure
|
||||
}
|
||||
| {
|
||||
entries: Array<{
|
||||
|
||||
@@ -404,6 +404,27 @@ function buildStructure(value, scope, path) {
|
||||
if (scope === 'config' && path === 'Phone.Keybind') {
|
||||
return { kind: 'optionalString' }
|
||||
}
|
||||
if (
|
||||
scope === 'config' &&
|
||||
path === 'Companies.Definitions' &&
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value)
|
||||
) {
|
||||
const keys = Object.keys(value).sort()
|
||||
const fields = Object.fromEntries(
|
||||
keys.map((key) => [
|
||||
key,
|
||||
buildStructure(value[key], scope, `${path}.${key}`),
|
||||
]),
|
||||
)
|
||||
return {
|
||||
fields,
|
||||
kind: 'table',
|
||||
mutableKeys: true,
|
||||
template: keys[0] ? fields[keys[0]] : undefined,
|
||||
}
|
||||
}
|
||||
if (
|
||||
scope === 'config' &&
|
||||
path === 'Garage.VehicleImages.ModelNames' &&
|
||||
@@ -452,7 +473,10 @@ function addField(fields, scope, path, value) {
|
||||
const sensitive = typeof value === 'string' && sensitivePath(path)
|
||||
fields.push({
|
||||
configured: sensitive ? value !== '' : undefined,
|
||||
label: humanize(path.split('.').at(-1)),
|
||||
label:
|
||||
scope === 'config' && path === 'Companies.Definitions'
|
||||
? 'Jobs'
|
||||
: humanize(path.split('.').at(-1)),
|
||||
path,
|
||||
scope,
|
||||
sensitive,
|
||||
@@ -470,12 +494,44 @@ function addField(fields, scope, path, value) {
|
||||
})
|
||||
}
|
||||
|
||||
function flattenCompanyFields(fields, path, value) {
|
||||
if (
|
||||
path === 'Companies.Definitions' ||
|
||||
value === null ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
value.__skyType ||
|
||||
Object.keys(value).length === 0
|
||||
) {
|
||||
addField(fields, 'config', path, value)
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of Object.keys(value).sort()) {
|
||||
flattenCompanyFields(fields, `${path}.${key}`, value[key])
|
||||
}
|
||||
}
|
||||
|
||||
function buildSections(scope, payload) {
|
||||
const sections = []
|
||||
const generalFields = []
|
||||
for (const key of Object.keys(payload).sort()) {
|
||||
const value = payload[key]
|
||||
if (
|
||||
if (scope === 'config' && key === 'Companies') {
|
||||
const fields = []
|
||||
flattenCompanyFields(fields, 'Companies', value)
|
||||
fields.sort((left, right) => {
|
||||
if (left.path === 'Companies.Definitions') return 1
|
||||
if (right.path === 'Companies.Definitions') return -1
|
||||
return left.path.localeCompare(right.path)
|
||||
})
|
||||
sections.push({
|
||||
fields,
|
||||
id: 'config:Companies',
|
||||
label: humanize(key),
|
||||
scope,
|
||||
})
|
||||
} else if (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
type ConfiguratorField = {
|
||||
label: string
|
||||
path: string
|
||||
structure?: ConfiguratorStructure
|
||||
type: string
|
||||
@@ -15,6 +16,8 @@ type ConfiguratorStructure = {
|
||||
fields?: Record<string, ConfiguratorStructure>
|
||||
items?: ConfiguratorStructure[]
|
||||
kind: string
|
||||
mutableKeys?: boolean
|
||||
template?: ConfiguratorStructure
|
||||
}
|
||||
|
||||
type ConfiguratorSection = {
|
||||
@@ -94,7 +97,9 @@ describe('admin configurator fixture', () => {
|
||||
)
|
||||
const darkChat = fields.find((field) => field.path === 'DarkChat')
|
||||
const phone = fields.find((field) => field.path === 'Phone')
|
||||
const companies = fields.find((field) => field.path === 'Companies')
|
||||
const companyJobs = fields.find(
|
||||
(field) => field.path === 'Companies.Definitions',
|
||||
)
|
||||
const garage = fields.find((field) => field.path === 'Garage')
|
||||
const timers = (darkChat?.value as Record<string, unknown> | undefined)
|
||||
?.AllowedDisappearTimers
|
||||
@@ -109,7 +114,16 @@ describe('admin configurator fixture', () => {
|
||||
})
|
||||
expect(darkChat?.structure?.fields?.AllowedDisappearTimers.kind).toBe('map')
|
||||
expect(phone?.structure?.fields?.Keybind.kind).toBe('optionalString')
|
||||
expect(companies?.structure?.fields?.Definitions.kind).toBe('table')
|
||||
expect(companyJobs?.label).toBe('Jobs')
|
||||
expect(companyJobs?.structure).toMatchObject({
|
||||
fields: {
|
||||
ambulance: { kind: 'table' },
|
||||
police: { kind: 'table' },
|
||||
},
|
||||
kind: 'table',
|
||||
mutableKeys: true,
|
||||
template: { kind: 'table' },
|
||||
})
|
||||
expect(
|
||||
garage?.structure?.fields?.VehicleImages.fields?.ModelNames.kind,
|
||||
).toBe('map')
|
||||
|
||||
@@ -235,10 +235,14 @@ local function upgrade_legacy_map(defaults, saved)
|
||||
return { __skyType = "map", entries = entries }
|
||||
end
|
||||
|
||||
local function merge_values(defaults, saved)
|
||||
local function merge_values(defaults, saved, path)
|
||||
path = path or ""
|
||||
if type(defaults) ~= "table" or type(saved) ~= "table" then
|
||||
return copy_value(saved)
|
||||
end
|
||||
if path == "Companies.Definitions" then
|
||||
return copy_value(saved)
|
||||
end
|
||||
if defaults.__skyType == "map" and not saved.__skyType then
|
||||
saved = upgrade_legacy_map(defaults, saved)
|
||||
end
|
||||
@@ -256,7 +260,9 @@ local function merge_values(defaults, saved)
|
||||
entries[#entries + 1] = {
|
||||
key = entry.key,
|
||||
keyType = entry.keyType,
|
||||
value = saved_entry and merge_values(entry.value, saved_entry.value) or copy_value(entry.value),
|
||||
value = saved_entry
|
||||
and merge_values(entry.value, saved_entry.value, path .. "." .. tostring(entry.key))
|
||||
or copy_value(entry.value),
|
||||
}
|
||||
included[identity] = true
|
||||
end
|
||||
@@ -274,7 +280,9 @@ local function merge_values(defaults, saved)
|
||||
if is_sequence(defaults) then
|
||||
local merged = copy_value(saved)
|
||||
for index, child in ipairs(defaults) do
|
||||
merged[index] = saved[index] ~= nil and merge_values(child, saved[index]) or copy_value(child)
|
||||
merged[index] = saved[index] ~= nil
|
||||
and merge_values(child, saved[index], path .. "." .. tostring(index))
|
||||
or copy_value(child)
|
||||
end
|
||||
return merged
|
||||
end
|
||||
@@ -285,7 +293,8 @@ local function merge_values(defaults, saved)
|
||||
local merged = copy_value(defaults)
|
||||
for key, child in pairs(saved) do
|
||||
if merged[key] ~= nil then
|
||||
merged[key] = merge_values(merged[key], child)
|
||||
local child_path = path == "" and tostring(key) or (path .. "." .. tostring(key))
|
||||
merged[key] = merge_values(merged[key], child, child_path)
|
||||
else
|
||||
merged[key] = copy_value(child)
|
||||
end
|
||||
@@ -440,6 +449,25 @@ local function build_structure(value, scope, path)
|
||||
if scope == "config" and path == "Phone.Keybind" then
|
||||
return { kind = "optionalString" }
|
||||
end
|
||||
if scope == "config" and path == "Companies.Definitions" and value_type == "table" then
|
||||
local keys = {}
|
||||
for key in pairs(value) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
table.sort(keys, function(left, right)
|
||||
return tostring(left) < tostring(right)
|
||||
end)
|
||||
local fields = {}
|
||||
for _, key in ipairs(keys) do
|
||||
fields[key] = build_structure(value[key], scope, path .. "." .. tostring(key))
|
||||
end
|
||||
return {
|
||||
fields = fields,
|
||||
kind = "table",
|
||||
mutableKeys = true,
|
||||
template = keys[1] and fields[keys[1]] or nil,
|
||||
}
|
||||
end
|
||||
if value_type ~= "table" then
|
||||
return {
|
||||
kind = "value",
|
||||
@@ -516,7 +544,9 @@ local function add_field(fields, field_index, scope, path, value, default_value)
|
||||
local sensitive = type(value) == "string" and sensitive_path(path)
|
||||
local field = {
|
||||
configured = sensitive and value ~= "" or nil,
|
||||
label = humanize(path:match("([^.]+)$") or path),
|
||||
label = scope == "config" and path == "Companies.Definitions"
|
||||
and "Jobs"
|
||||
or humanize(path:match("([^.]+)$") or path),
|
||||
path = path,
|
||||
scope = scope,
|
||||
sensitive = sensitive,
|
||||
@@ -528,6 +558,35 @@ local function add_field(fields, field_index, scope, path, value, default_value)
|
||||
field_index[scope .. ":" .. path] = field
|
||||
end
|
||||
|
||||
local function flatten_company_fields(fields, field_index, path, value, default_value)
|
||||
if path == "Companies.Definitions"
|
||||
or type(value) ~= "table"
|
||||
or value.__skyType
|
||||
or is_sequence(value)
|
||||
or next(value) == nil
|
||||
then
|
||||
add_field(fields, field_index, "config", path, value, default_value)
|
||||
return
|
||||
end
|
||||
|
||||
local keys = {}
|
||||
for key in pairs(value) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
table.sort(keys, function(left, right)
|
||||
return tostring(left) < tostring(right)
|
||||
end)
|
||||
for _, key in ipairs(keys) do
|
||||
flatten_company_fields(
|
||||
fields,
|
||||
field_index,
|
||||
path .. "." .. tostring(key),
|
||||
value[key],
|
||||
type(default_value) == "table" and default_value[key] or nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function build_sections(scope, payload, defaults, sections, field_index)
|
||||
local general_fields = {}
|
||||
local keys = {}
|
||||
@@ -540,7 +599,25 @@ local function build_sections(scope, payload, defaults, sections, field_index)
|
||||
|
||||
for _, key in ipairs(keys) do
|
||||
local value = payload[key]
|
||||
if type(value) == "table" and not value.__skyType and not is_sequence(value) and next(value) ~= nil then
|
||||
if scope == "config" and key == "Companies" then
|
||||
local fields = {}
|
||||
flatten_company_fields(fields, field_index, "Companies", value, defaults[key])
|
||||
table.sort(fields, function(left, right)
|
||||
if left.path == "Companies.Definitions" then
|
||||
return false
|
||||
end
|
||||
if right.path == "Companies.Definitions" then
|
||||
return true
|
||||
end
|
||||
return left.path < right.path
|
||||
end)
|
||||
sections[#sections + 1] = {
|
||||
fields = fields,
|
||||
id = "config:Companies",
|
||||
label = humanize(key),
|
||||
scope = scope,
|
||||
}
|
||||
elseif type(value) == "table" and not value.__skyType and not is_sequence(value) and next(value) ~= nil then
|
||||
local fields = {}
|
||||
add_field(fields, field_index, scope, tostring(key), value, defaults[key])
|
||||
sections[#sections + 1] = {
|
||||
@@ -728,9 +805,22 @@ local function validate_locked_structure(structure, value)
|
||||
if type(value) ~= "table" or value.__skyType or is_sequence(value) then
|
||||
return false
|
||||
end
|
||||
for key, field_structure in pairs(structure.fields or {}) do
|
||||
if value[key] == nil or not validate_locked_structure(field_structure, value[key]) then
|
||||
return false
|
||||
if not structure.mutableKeys then
|
||||
for key, field_structure in pairs(structure.fields or {}) do
|
||||
if value[key] == nil or not validate_locked_structure(field_structure, value[key]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
if structure.mutableKeys and structure.template then
|
||||
for key, child in pairs(value) do
|
||||
if type(key) ~= "string"
|
||||
or #key > 64
|
||||
or not key:match("^[a-z0-9_-]+$")
|
||||
or not validate_locked_structure(structure.template, child)
|
||||
then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
@@ -832,8 +922,8 @@ if not row then
|
||||
error("[sky_phone] Phone configurator could not load its SQL row after initialization.")
|
||||
end
|
||||
|
||||
stored_config = merge_values(default_config, decode_payload(row.config_payload, "config"))
|
||||
stored_media = merge_values(default_media, decode_payload(row.media_payload, "media"))
|
||||
stored_config = merge_values(default_config, decode_payload(row.config_payload, "config"), "")
|
||||
stored_media = merge_values(default_media, decode_payload(row.media_payload, "media"), "")
|
||||
revision = tonumber(row.revision) or 1
|
||||
updated_at = row.updated_at
|
||||
updated_by_name = row.updated_by_name
|
||||
|
||||
Reference in New Issue
Block a user