mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 23:01:37 +00:00
FIX - repair company actions and key rebindings (#32)
* FIX - repair company detail actions Constrain compact navbar titles to their grid column so long company names cannot overlap the back action. Allow configured public emergency companies to accept normal service requests, and migrate existing police profile and Phone Configurator defaults exactly once. * FIX - preserve phone key rebindings Use the stable sky_phone_toggle command as the RegisterKeyMapping identifier. The previous revisioned command names detached FiveM's persisted keyboard settings from the active handler whenever the mapping identity changed.
This commit is contained in:
@@ -0,0 +1,147 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
const config = readFileSync(
|
||||||
|
new URL('../../sky_phone/config/config.lua', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
).replace(/\r\n/g, '\n')
|
||||||
|
const companiesServer = readFileSync(
|
||||||
|
new URL('../../sky_phone/source/server/companies.lua', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
).replace(/\r\n/g, '\n')
|
||||||
|
const configuratorServer = readFileSync(
|
||||||
|
new URL(
|
||||||
|
'../../sky_phone/source/server/phone_configurator.lua',
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
).replace(/\r\n/g, '\n')
|
||||||
|
const testServer = readFileSync(
|
||||||
|
new URL('../testserver/index.cjs', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
).replace(/\r\n/g, '\n')
|
||||||
|
|
||||||
|
function sourceBlock(source: string, startMarker: string, endMarker: string) {
|
||||||
|
const start = source.indexOf(startMarker)
|
||||||
|
const end = source.indexOf(endMarker, start)
|
||||||
|
|
||||||
|
expect(start).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(end).toBeGreaterThan(start)
|
||||||
|
return source.slice(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Companies emergency request contract', () => {
|
||||||
|
it('ships non-emergency police assistance as a requestable service', () => {
|
||||||
|
const police = sourceBlock(
|
||||||
|
config,
|
||||||
|
' police = {',
|
||||||
|
' ambulance = {',
|
||||||
|
)
|
||||||
|
const mockPolice = sourceBlock(
|
||||||
|
testServer,
|
||||||
|
'const companyProfiles = [',
|
||||||
|
" {\n acceptsRequests: false,\n announcement: null,",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(police).toContain('Emergency = true')
|
||||||
|
expect(police).toContain('AcceptsRequests = true')
|
||||||
|
expect(police).toContain('Id = "police-assistance"')
|
||||||
|
expect(police).toContain('RequestsEnabled = true')
|
||||||
|
expect(mockPolice).toContain('acceptsRequests: true')
|
||||||
|
expect(mockPolice).toContain("name: 'Los Santos Police Department'")
|
||||||
|
expect(mockPolice).toContain("id: 'police-assistance'")
|
||||||
|
})
|
||||||
|
|
||||||
|
it('authorizes configured emergency companies through the normal request gates', () => {
|
||||||
|
const validation = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'local function validate_configuration()',
|
||||||
|
'local function seed_companies()',
|
||||||
|
)
|
||||||
|
const payload = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'local function company_payload(',
|
||||||
|
'local function public_company(',
|
||||||
|
)
|
||||||
|
const createRequest = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:companies:create-request"',
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:companies:cancel-request"',
|
||||||
|
)
|
||||||
|
const updateProfile = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:companies:update-profile"',
|
||||||
|
'Bridge.Callbacks.Register("sky_phone:companies:update-hours"',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(validation).not.toContain(
|
||||||
|
'definition.Emergency and definition.AcceptsRequests',
|
||||||
|
)
|
||||||
|
expect(payload).toContain(
|
||||||
|
'acceptsRequests = tonumber(row.accepts_requests) == 1,',
|
||||||
|
)
|
||||||
|
expect(payload).not.toContain('not definition.Emergency')
|
||||||
|
expect(createRequest).toContain(
|
||||||
|
'if not definition or not definition.Public then',
|
||||||
|
)
|
||||||
|
expect(createRequest).not.toContain('definition.Emergency')
|
||||||
|
expect(createRequest).toContain('SELECT `accepts_requests`')
|
||||||
|
expect(createRequest).toContain('AND `requests_enabled` = 1')
|
||||||
|
expect(updateProfile).not.toContain('member.definition.Emergency')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('migrates existing requestable emergency profiles exactly once', () => {
|
||||||
|
const migration = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'local function migrate_requestable_emergency_companies()',
|
||||||
|
'local function tombstone_removed_companies()',
|
||||||
|
)
|
||||||
|
const refresh = sourceBlock(
|
||||||
|
companiesServer,
|
||||||
|
'local function refresh_runtime_configuration()',
|
||||||
|
'\n\nrefresh_runtime_configuration()',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(migration).toContain(
|
||||||
|
'sky-phone:companies:requestable-emergency:v1',
|
||||||
|
)
|
||||||
|
expect(migration).toContain(
|
||||||
|
'if definition.Emergency and definition.AcceptsRequests then',
|
||||||
|
)
|
||||||
|
expect(migration).toContain('SET `accepts_requests` = 1')
|
||||||
|
expect(migration).toContain('INSERT IGNORE INTO `sky_phone_migrations`')
|
||||||
|
expect(migration).toContain('Bridge.Database.Transaction(statements)')
|
||||||
|
expect(refresh.indexOf('seed_companies()')).toBeLessThan(
|
||||||
|
refresh.indexOf('migrate_requestable_emergency_companies()'),
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
refresh.indexOf('migrate_requestable_emergency_companies()'),
|
||||||
|
).toBeLessThan(refresh.indexOf('tombstone_removed_companies()'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('migrates the existing Phone Configurator police defaults', () => {
|
||||||
|
const migration = sourceBlock(
|
||||||
|
configuratorServer,
|
||||||
|
'local function migrate_police_request_defaults()',
|
||||||
|
'\n\ndefault_config = {}',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(migration).toContain('sky-phone:configurator:police-requests:v1')
|
||||||
|
expect(migration).toContain('police.AcceptsRequests == false')
|
||||||
|
expect(migration).toContain('next(police.Services) == nil')
|
||||||
|
expect(migration).toContain(
|
||||||
|
'police.AcceptsRequests = defaults.AcceptsRequests',
|
||||||
|
)
|
||||||
|
expect(migration).toContain('police.Services = copy_value(defaults.Services)')
|
||||||
|
expect(migration).toContain('SET `config_payload` = ?')
|
||||||
|
expect(migration).toContain('`revision` = `revision` + 1')
|
||||||
|
expect(migration).toContain('INSERT IGNORE INTO `sky_phone_migrations`')
|
||||||
|
expect(migration).toContain('Bridge.Database.Transaction(statements)')
|
||||||
|
expect(migration).toContain('apply_stored_row(read_stored_row())')
|
||||||
|
expect(migration).toContain('apply_runtime_configuration()')
|
||||||
|
expect(configuratorServer).toContain(
|
||||||
|
'Bridge.Database.AfterMigration("sky_phone", migrate_police_request_defaults)',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -286,19 +286,19 @@ describe('phone inventory contracts', () => {
|
|||||||
expect(phoneBridge).toContain('TriggerEvent("lb-phone:deletedFromGallery"')
|
expect(phoneBridge).toContain('TriggerEvent("lb-phone:deletedFromGallery"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens from a configurable F1 mapping without client-provided device identity', () => {
|
it('keeps the phone key mapping command stable so FiveM user rebindings persist', () => {
|
||||||
const config = readResourceFile('config/config.lua')
|
const config = readResourceFile('config/config.lua')
|
||||||
const phoneClient = readResourceFile('source/client/main.lua')
|
const phoneClient = readResourceFile('source/client/main.lua')
|
||||||
const phoneServer = readResourceFile('source/server/phone.lua')
|
const phoneServer = readResourceFile('source/server/phone.lua')
|
||||||
|
|
||||||
expect(config).toContain('Keybind = "F1"')
|
expect(config).toContain('Keybind = "F1"')
|
||||||
|
expect(phoneClient).toContain('local phone_key_mapping_registered = false')
|
||||||
expect(phoneClient).toContain('refresh_phone_key_mapping = function()')
|
expect(phoneClient).toContain('refresh_phone_key_mapping = function()')
|
||||||
expect(phoneClient).toContain(
|
expect(phoneClient).toMatch(
|
||||||
'RegisterKeyMapping(command_name, locale.Controls.OpenPhone, "keyboard", key_name)',
|
/RegisterKeyMapping\(\s*"sky_phone_toggle",\s*locale\.Controls\.OpenPhone,\s*"keyboard",\s*key_name\s*\)/,
|
||||||
)
|
|
||||||
expect(phoneClient).toContain(
|
|
||||||
'if active_key_mapping_command == command_name then',
|
|
||||||
)
|
)
|
||||||
|
expect(phoneClient).not.toContain('sky_phone_toggle_config_')
|
||||||
|
expect(phoneClient).not.toContain('key_mapping_revision')
|
||||||
expect(phoneClient).toContain(
|
expect(phoneClient).toContain(
|
||||||
'request_phone_open("sky_phone:device:open-request")',
|
'request_phone_open("sky_phone:device:open-request")',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,6 +43,17 @@ describe('SkyNavbar', () => {
|
|||||||
expect(html).toContain('Account')
|
expect(html).toContain('Account')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('constrains long compact titles to the center column', () => {
|
||||||
|
const titleRule = foundationStyles.match(
|
||||||
|
/\.sky-navbar__title\s*\{([^}]*)\}/s,
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
expect(titleRule).toContain('max-width: 100%')
|
||||||
|
expect(titleRule).toContain('overflow: hidden')
|
||||||
|
expect(titleRule).toContain('text-overflow: ellipsis')
|
||||||
|
expect(titleRule).toContain('white-space: nowrap')
|
||||||
|
})
|
||||||
|
|
||||||
it('exposes the large-title header without changing heading semantics', async () => {
|
it('exposes the large-title header without changing heading semantics', async () => {
|
||||||
const html = await renderToString(
|
const html = await renderToString(
|
||||||
createSSRApp(SkyNavbar, {
|
createSSRApp(SkyNavbar, {
|
||||||
|
|||||||
Vendored
+1
@@ -168,6 +168,7 @@
|
|||||||
|
|
||||||
.sky-navbar__title {
|
.sky-navbar__title {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 var(--sky-space-1);
|
padding: 0 var(--sky-space-1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -135,6 +135,49 @@ describe('admin configurator fixture', () => {
|
|||||||
).toBe('map')
|
).toBe('map')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('exposes the requestable police assistance defaults', () => {
|
||||||
|
const companyJobs = loadConfiguratorSections()
|
||||||
|
.flatMap((section) => section.fields)
|
||||||
|
.find((field) => field.path === 'Companies.Definitions')
|
||||||
|
const police = (
|
||||||
|
companyJobs?.value as
|
||||||
|
| Record<string, Record<string, unknown>>
|
||||||
|
| undefined
|
||||||
|
)?.police
|
||||||
|
|
||||||
|
expect(police).toMatchObject({
|
||||||
|
AcceptsRequests: true,
|
||||||
|
Emergency: true,
|
||||||
|
Services: [
|
||||||
|
{
|
||||||
|
Id: 'police-assistance',
|
||||||
|
RequestsEnabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
companyJobs?.structure?.fields?.police.fields?.Services,
|
||||||
|
).toMatchObject({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
fields: {
|
||||||
|
Id: { kind: 'value', valueType: 'string' },
|
||||||
|
RequestsEnabled: { kind: 'value', valueType: 'boolean' },
|
||||||
|
},
|
||||||
|
kind: 'table',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
kind: 'list',
|
||||||
|
template: {
|
||||||
|
fields: {
|
||||||
|
Id: { kind: 'value', valueType: 'string' },
|
||||||
|
RequestsEnabled: { kind: 'value', valueType: 'boolean' },
|
||||||
|
},
|
||||||
|
kind: 'table',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('publishes fixed schemas for every empty configurable collection', () => {
|
it('publishes fixed schemas for every empty configurable collection', () => {
|
||||||
const fields = loadConfiguratorSections().flatMap(
|
const fields = loadConfiguratorSections().flatMap(
|
||||||
(section) => section.fields,
|
(section) => section.fields,
|
||||||
@@ -183,7 +226,7 @@ describe('admin configurator fixture', () => {
|
|||||||
template: { kind: 'value', valueType: 'string' },
|
template: { kind: 'value', valueType: 'string' },
|
||||||
})
|
})
|
||||||
expect(
|
expect(
|
||||||
root('Companies.Definitions')?.fields?.police.fields?.Services,
|
root('Companies.Definitions')?.fields?.ambulance.fields?.Services,
|
||||||
).toMatchObject({
|
).toMatchObject({
|
||||||
items: [],
|
items: [],
|
||||||
kind: 'list',
|
kind: 'list',
|
||||||
|
|||||||
@@ -4178,7 +4178,7 @@ const companyCategories = [
|
|||||||
let companyCallAvailable = false
|
let companyCallAvailable = false
|
||||||
const companyProfiles = [
|
const companyProfiles = [
|
||||||
{
|
{
|
||||||
acceptsRequests: false,
|
acceptsRequests: true,
|
||||||
announcement: {
|
announcement: {
|
||||||
body: 'Community traffic unit active around Legion Square.',
|
body: 'Community traffic unit active around Legion Square.',
|
||||||
expiresAt: isoTime(6 * 60 * 60 * 1000),
|
expiresAt: isoTime(6 * 60 * 60 * 1000),
|
||||||
@@ -4202,28 +4202,20 @@ const companyProfiles = [
|
|||||||
label: 'Mission Row Police Station',
|
label: 'Mission Row Police Station',
|
||||||
},
|
},
|
||||||
logoUrl: 'https://picsum.photos/seed/companies-police-logo/180/180',
|
logoUrl: 'https://picsum.photos/seed/companies-police-logo/180/180',
|
||||||
name: 'Los Santos Police',
|
name: 'Los Santos Police Department',
|
||||||
phoneNumber: '911',
|
phoneNumber: '911',
|
||||||
revision: 3,
|
revision: 3,
|
||||||
services: [
|
services: [
|
||||||
{
|
{
|
||||||
acceptsRequests: false,
|
acceptsRequests: true,
|
||||||
active: true,
|
active: true,
|
||||||
description: 'Immediate police response through the service line.',
|
description: 'Request non-emergency police assistance.',
|
||||||
id: 'emergency-response',
|
id: 'police-assistance',
|
||||||
priceText: null,
|
priceText: null,
|
||||||
title: 'Emergency Response',
|
title: 'Police Assistance',
|
||||||
},
|
|
||||||
{
|
|
||||||
acceptsRequests: false,
|
|
||||||
active: true,
|
|
||||||
description: 'General information and non-emergency assistance.',
|
|
||||||
id: 'public-assistance',
|
|
||||||
priceText: null,
|
|
||||||
title: 'Public Assistance',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
serviceSummary: 'Emergency response and public assistance',
|
serviceSummary: 'Non-emergency police assistance',
|
||||||
verified: true,
|
verified: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1181,7 +1181,7 @@ if IsDuplicityVersion() then
|
|||||||
LogoUrl = "https://picsum.photos/seed/companies-police-logo/180/180",
|
LogoUrl = "https://picsum.photos/seed/companies-police-logo/180/180",
|
||||||
Description = "Public safety, emergency response, and police services.",
|
Description = "Public safety, emergency response, and police services.",
|
||||||
DefaultAvailability = "closed",
|
DefaultAvailability = "closed",
|
||||||
AcceptsRequests = false,
|
AcceptsRequests = true,
|
||||||
District = "Mission Row",
|
District = "Mission Row",
|
||||||
LocationLabel = "Mission Row Police Station",
|
LocationLabel = "Mission Row Police Station",
|
||||||
Address = "Mission Row Police Station",
|
Address = "Mission Row Police Station",
|
||||||
@@ -1203,7 +1203,15 @@ if IsDuplicityVersion() then
|
|||||||
Services = 3,
|
Services = 3,
|
||||||
Announcement = 3,
|
Announcement = 3,
|
||||||
},
|
},
|
||||||
Services = {},
|
Services = {
|
||||||
|
{
|
||||||
|
Id = "police-assistance",
|
||||||
|
Title = "Police assistance",
|
||||||
|
Description = "Request non-emergency police assistance.",
|
||||||
|
Price = "",
|
||||||
|
RequestsEnabled = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ambulance = {
|
ambulance = {
|
||||||
Job = "ambulance",
|
Job = "ambulance",
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ local suggested_admin_command = nil
|
|||||||
local suggested_test_data_command = nil
|
local suggested_test_data_command = nil
|
||||||
local active_development_command = nil
|
local active_development_command = nil
|
||||||
local registered_development_commands = {}
|
local registered_development_commands = {}
|
||||||
local active_key_mapping_command = nil
|
local phone_key_mapping_registered = false
|
||||||
local active_key_mapping_key = nil
|
|
||||||
local key_mapping_revision = 0
|
|
||||||
local refresh_development_command
|
local refresh_development_command
|
||||||
local refresh_phone_key_mapping
|
local refresh_phone_key_mapping
|
||||||
local refresh_test_data_command_suggestion
|
local refresh_test_data_command_suggestion
|
||||||
@@ -312,32 +310,30 @@ RegisterCommand("sky_phone_live_activity_open", function()
|
|||||||
end
|
end
|
||||||
end, false)
|
end, false)
|
||||||
|
|
||||||
RegisterCommand("sky_phone_toggle", run_phone_toggle, false)
|
RegisterCommand("sky_phone_toggle", function()
|
||||||
|
if not Config.Phone.Keybind then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
run_phone_toggle()
|
||||||
|
end, false)
|
||||||
|
|
||||||
refresh_phone_key_mapping = function()
|
refresh_phone_key_mapping = function()
|
||||||
local key_name = Config.Phone.Keybind
|
local key_name = Config.Phone.Keybind
|
||||||
if key_name ~= false and key_name ~= nil and (type(key_name) ~= "string" or key_name == "") then
|
if key_name ~= false and key_name ~= nil and (type(key_name) ~= "string" or key_name == "") then
|
||||||
error("[sky_phone] Config.Phone.Keybind must be a non-empty keyboard key name or false.")
|
error("[sky_phone] Config.Phone.Keybind must be a non-empty keyboard key name or false.")
|
||||||
end
|
end
|
||||||
if key_name == active_key_mapping_key then
|
if phone_key_mapping_registered or not key_name then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
active_key_mapping_key = key_name
|
-- FiveM persists player rebindings by command name, so this identifier must remain stable.
|
||||||
active_key_mapping_command = nil
|
phone_key_mapping_registered = true
|
||||||
if not key_name then
|
RegisterKeyMapping(
|
||||||
return
|
"sky_phone_toggle",
|
||||||
end
|
locale.Controls.OpenPhone,
|
||||||
|
"keyboard",
|
||||||
key_mapping_revision = key_mapping_revision + 1
|
key_name
|
||||||
local command_name = "sky_phone_toggle_config_" .. key_mapping_revision
|
)
|
||||||
active_key_mapping_command = command_name
|
|
||||||
RegisterCommand(command_name, function()
|
|
||||||
if active_key_mapping_command == command_name then
|
|
||||||
run_phone_toggle()
|
|
||||||
end
|
|
||||||
end, false)
|
|
||||||
RegisterKeyMapping(command_name, locale.Controls.OpenPhone, "keyboard", key_name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
refresh_phone_key_mapping()
|
refresh_phone_key_mapping()
|
||||||
|
|||||||
@@ -279,7 +279,6 @@ local function validate_configuration()
|
|||||||
or not Config.Companies.AvailabilityStatuses[definition.DefaultAvailability]
|
or not Config.Companies.AvailabilityStatuses[definition.DefaultAvailability]
|
||||||
or not valid_text(definition.Icon, 64, false)
|
or not valid_text(definition.Icon, 64, false)
|
||||||
or not logo_url or not logo_url:match("^https://[^%s]+$")
|
or not logo_url or not logo_url:match("^https://[^%s]+$")
|
||||||
or (definition.Emergency and definition.AcceptsRequests)
|
|
||||||
then
|
then
|
||||||
error(("[sky_phone] Company definition '%s' has invalid public profile defaults."):format(company_id))
|
error(("[sky_phone] Company definition '%s' has invalid public profile defaults."):format(company_id))
|
||||||
end
|
end
|
||||||
@@ -466,6 +465,48 @@ local function seed_companies()
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function migrate_requestable_emergency_companies()
|
||||||
|
local migration_name = "sky-phone:companies:requestable-emergency:v1"
|
||||||
|
local completed = Bridge.Database.Query(
|
||||||
|
"SELECT 1 FROM `sky_phone_migrations` WHERE `name` = ? LIMIT 1",
|
||||||
|
{ migration_name }
|
||||||
|
)
|
||||||
|
if completed[1] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local statements = {}
|
||||||
|
local migrated_companies = {}
|
||||||
|
for _, company_id in ipairs(definition_ids) do
|
||||||
|
local definition = definitions[company_id]
|
||||||
|
if definition.Emergency and definition.AcceptsRequests then
|
||||||
|
statements[#statements + 1] = {
|
||||||
|
query = [[
|
||||||
|
UPDATE `sky_phone_company_profiles`
|
||||||
|
SET `accepts_requests` = 1, `revision` = `revision` + 1
|
||||||
|
WHERE `company_id` = ? AND `accepts_requests` = 0
|
||||||
|
]],
|
||||||
|
params = { company_id },
|
||||||
|
}
|
||||||
|
migrated_companies[#migrated_companies + 1] = company_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
statements[#statements + 1] = {
|
||||||
|
query = [[
|
||||||
|
INSERT IGNORE INTO `sky_phone_migrations` (`name`, `source`, `stats`)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
]],
|
||||||
|
params = {
|
||||||
|
migration_name,
|
||||||
|
"sky-phone",
|
||||||
|
json.encode({ companies = migrated_companies }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if not Bridge.Database.Transaction(statements) then
|
||||||
|
error("[sky_phone] Could not migrate requestable emergency company profiles.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function tombstone_removed_companies()
|
local function tombstone_removed_companies()
|
||||||
local profiles = Bridge.Database.Query([[
|
local profiles = Bridge.Database.Query([[
|
||||||
SELECT DISTINCT profile.`company_id`
|
SELECT DISTINCT profile.`company_id`
|
||||||
@@ -826,7 +867,7 @@ local function company_payload(company_id, include_inactive_services)
|
|||||||
availability = availability,
|
availability = availability,
|
||||||
availabilityUpdatedAt = iso_time(row.availability_updated_at_unix)
|
availabilityUpdatedAt = iso_time(row.availability_updated_at_unix)
|
||||||
or iso_time(row.updated_at_unix),
|
or iso_time(row.updated_at_unix),
|
||||||
acceptsRequests = tonumber(row.accepts_requests) == 1 and not definition.Emergency,
|
acceptsRequests = tonumber(row.accepts_requests) == 1,
|
||||||
phoneNumber = line and line.Number or nil,
|
phoneNumber = line and line.Number or nil,
|
||||||
canCall = line and line.CanCall == true or false,
|
canCall = line and line.CanCall == true or false,
|
||||||
canMessage = line and line.CanMessage == true or false,
|
canMessage = line and line.CanMessage == true or false,
|
||||||
@@ -912,6 +953,7 @@ local function refresh_runtime_configuration()
|
|||||||
service_lines_by_number = {}
|
service_lines_by_number = {}
|
||||||
validate_configuration()
|
validate_configuration()
|
||||||
seed_companies()
|
seed_companies()
|
||||||
|
migrate_requestable_emergency_companies()
|
||||||
tombstone_removed_companies()
|
tombstone_removed_companies()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1839,7 +1881,7 @@ Bridge.Callbacks.Register("sky_phone:companies:create-request", function(source,
|
|||||||
end
|
end
|
||||||
local company_id = data.companyId
|
local company_id = data.companyId
|
||||||
local definition = type(company_id) == "string" and definitions[company_id] or nil
|
local definition = type(company_id) == "string" and definitions[company_id] or nil
|
||||||
if not definition or not definition.Public or definition.Emergency then
|
if not definition or not definition.Public then
|
||||||
return { success = false, error = "company_not_found" }
|
return { success = false, error = "company_not_found" }
|
||||||
end
|
end
|
||||||
local subject = valid_text(data.subject, Config.Companies.SubjectMaxLength, false)
|
local subject = valid_text(data.subject, Config.Companies.SubjectMaxLength, false)
|
||||||
@@ -2636,7 +2678,6 @@ Bridge.Callbacks.Register("sky_phone:companies:update-profile", function(source,
|
|||||||
local address = valid_text(data.address, Config.Companies.AddressMaxLength, true)
|
local address = valid_text(data.address, Config.Companies.AddressMaxLength, true)
|
||||||
if not revision or not description or not district or not location_label or not address
|
if not revision or not description or not district or not location_label or not address
|
||||||
or type(data.acceptsRequests) ~= "boolean"
|
or type(data.acceptsRequests) ~= "boolean"
|
||||||
or (member.definition.Emergency and data.acceptsRequests)
|
|
||||||
then
|
then
|
||||||
return { success = false, error = "invalid_profile" }
|
return { success = false, error = "invalid_profile" }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1075,6 +1075,69 @@ local function apply_stored_row(row)
|
|||||||
updated_by_name = row.updated_by_name
|
updated_by_name = row.updated_by_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function migrate_police_request_defaults()
|
||||||
|
local migration_name = "sky-phone:configurator:police-requests:v1"
|
||||||
|
local completed = Bridge.Database.Query(
|
||||||
|
"SELECT 1 FROM `sky_phone_migrations` WHERE `name` = ? LIMIT 1",
|
||||||
|
{ migration_name }
|
||||||
|
)
|
||||||
|
if completed[1] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local row = read_stored_row()
|
||||||
|
local config_payload = decode_payload(row.config_payload, "config")
|
||||||
|
local police = config_payload.Companies
|
||||||
|
and config_payload.Companies.Definitions
|
||||||
|
and config_payload.Companies.Definitions.police
|
||||||
|
local migrated = type(police) == "table"
|
||||||
|
and police.Emergency == true
|
||||||
|
and police.AcceptsRequests == false
|
||||||
|
and type(police.Services) == "table"
|
||||||
|
and next(police.Services) == nil
|
||||||
|
local statements = {}
|
||||||
|
if migrated then
|
||||||
|
local defaults = default_config.Companies.Definitions.police
|
||||||
|
police.AcceptsRequests = defaults.AcceptsRequests
|
||||||
|
police.Services = copy_value(defaults.Services)
|
||||||
|
statements[#statements + 1] = {
|
||||||
|
query = ([[
|
||||||
|
UPDATE `%s`
|
||||||
|
SET `config_payload` = ?, `revision` = `revision` + 1
|
||||||
|
WHERE `id` = ?
|
||||||
|
]]):format(TABLE_NAME),
|
||||||
|
params = { encode_payload(config_payload, "config"), CONFIG_ROW_ID },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
statements[#statements + 1] = {
|
||||||
|
query = [[
|
||||||
|
INSERT IGNORE INTO `sky_phone_migrations` (`name`, `source`, `stats`)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
]],
|
||||||
|
params = {
|
||||||
|
migration_name,
|
||||||
|
"sky-phone",
|
||||||
|
json.encode({ migrated = migrated }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if not Bridge.Database.Transaction(statements) then
|
||||||
|
error("[sky_phone] Could not migrate Phone Configurator police request defaults.")
|
||||||
|
end
|
||||||
|
if not migrated then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
apply_stored_row(read_stored_row())
|
||||||
|
apply_runtime_configuration()
|
||||||
|
TriggerEvent("sky_phone:configurator:serverUpdated", revision)
|
||||||
|
SkyPhoneConfigurator.Broadcast(-1)
|
||||||
|
Bridge.Debug(
|
||||||
|
"info",
|
||||||
|
"[sky_phone] Migrated Phone Configurator police request defaults.",
|
||||||
|
{ always = true }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
default_config = {}
|
default_config = {}
|
||||||
for key, value in pairs(ConfigDefaults) do
|
for key, value in pairs(ConfigDefaults) do
|
||||||
if key ~= "Media" and key ~= "PhoneConfigurator" and key ~= "CommandPermissions" then
|
if key ~= "Media" and key ~= "PhoneConfigurator" and key ~= "CommandPermissions" then
|
||||||
@@ -1095,6 +1158,7 @@ Bridge.Database.Query(([[
|
|||||||
|
|
||||||
apply_stored_row(read_stored_row())
|
apply_stored_row(read_stored_row())
|
||||||
apply_runtime_configuration()
|
apply_runtime_configuration()
|
||||||
|
Bridge.Database.AfterMigration("sky_phone", migrate_police_request_defaults)
|
||||||
|
|
||||||
function SkyPhoneConfigurator.GetAdminData()
|
function SkyPhoneConfigurator.GetAdminData()
|
||||||
local data = build_admin_data()
|
local data = build_admin_data()
|
||||||
|
|||||||
@@ -1166,7 +1166,7 @@ if IsDuplicityVersion() then
|
|||||||
LogoUrl = "https://picsum.photos/seed/companies-police-logo/180/180",
|
LogoUrl = "https://picsum.photos/seed/companies-police-logo/180/180",
|
||||||
Description = "Public safety, emergency response, and police services.",
|
Description = "Public safety, emergency response, and police services.",
|
||||||
DefaultAvailability = "closed",
|
DefaultAvailability = "closed",
|
||||||
AcceptsRequests = false,
|
AcceptsRequests = true,
|
||||||
District = "Mission Row",
|
District = "Mission Row",
|
||||||
LocationLabel = "Mission Row Police Station",
|
LocationLabel = "Mission Row Police Station",
|
||||||
Address = "Mission Row Police Station",
|
Address = "Mission Row Police Station",
|
||||||
@@ -1188,7 +1188,15 @@ if IsDuplicityVersion() then
|
|||||||
Services = 3,
|
Services = 3,
|
||||||
Announcement = 3,
|
Announcement = 3,
|
||||||
},
|
},
|
||||||
Services = {},
|
Services = {
|
||||||
|
{
|
||||||
|
Id = "police-assistance",
|
||||||
|
Title = "Police assistance",
|
||||||
|
Description = "Request non-emergency police assistance.",
|
||||||
|
Price = "",
|
||||||
|
RequestsEnabled = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ambulance = {
|
ambulance = {
|
||||||
Job = "ambulance",
|
Job = "ambulance",
|
||||||
|
|||||||
Reference in New Issue
Block a user