863 lines
29 KiB
TypeScript
863 lines
29 KiB
TypeScript
import { router, useForm } from '@inertiajs/react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { createOperationKey } from '@/lib/operation-key';
|
|
import type { Partnership } from '@/pages/partnerships/shared';
|
|
|
|
export type PartnershipLocation = {
|
|
geo_location_id: string;
|
|
role: string;
|
|
source_reference: string;
|
|
};
|
|
export type PartnershipOrigin = { type: string; target_id: string };
|
|
export type PartnershipEditProps = {
|
|
editorActorId?: number;
|
|
saveCommands?: { key: string; version: number }[];
|
|
record: Partnership | null;
|
|
locations: PartnershipLocation[];
|
|
origins: PartnershipOrigin[];
|
|
};
|
|
type Payload = {
|
|
summary: string;
|
|
requirements: string;
|
|
mandatory: string[];
|
|
confidentiality: string;
|
|
consent: boolean;
|
|
availability: string;
|
|
};
|
|
type Sections = Payload & {
|
|
title: string;
|
|
offer: string;
|
|
seeking: string;
|
|
organization_id: string;
|
|
sector_id: string;
|
|
kind: string;
|
|
partnership_type: string;
|
|
deadline: string;
|
|
locations: PartnershipLocation[];
|
|
origins: PartnershipOrigin[];
|
|
};
|
|
type Draft = Omit<Sections, keyof Payload> & {
|
|
payload: Payload;
|
|
client_key: string;
|
|
version: number | null;
|
|
};
|
|
type Snapshot = {
|
|
actorId: number | null;
|
|
commands: { key: string; version: number }[];
|
|
id: number | null;
|
|
version: number | null;
|
|
status: string;
|
|
clientKey: string | null;
|
|
values: Sections;
|
|
};
|
|
const textKeys = [
|
|
'title',
|
|
'offer',
|
|
'seeking',
|
|
'organization_id',
|
|
'sector_id',
|
|
'kind',
|
|
'partnership_type',
|
|
'deadline',
|
|
'summary',
|
|
'requirements',
|
|
'confidentiality',
|
|
'availability',
|
|
] as const;
|
|
const keys = [
|
|
...textKeys,
|
|
'mandatory',
|
|
'consent',
|
|
'locations',
|
|
'origins',
|
|
] as const;
|
|
export type PartnershipSection = (typeof keys)[number];
|
|
const editableStates = ['draft', 'returned', 'rejected', 'published'];
|
|
function object(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
function text(value: unknown, fallback = '') {
|
|
return value == null
|
|
? fallback
|
|
: typeof value === 'string'
|
|
? value
|
|
: undefined;
|
|
}
|
|
function bool(value: unknown) {
|
|
return value == null
|
|
? false
|
|
: value === true || value === 1 || value === '1'
|
|
? true
|
|
: value === false || value === 0 || value === '0'
|
|
? false
|
|
: undefined;
|
|
}
|
|
function location(value: unknown): value is PartnershipLocation {
|
|
return (
|
|
object(value) &&
|
|
typeof value.geo_location_id === 'string' &&
|
|
typeof value.role === 'string' &&
|
|
typeof value.source_reference === 'string'
|
|
);
|
|
}
|
|
function origin(value: unknown): value is PartnershipOrigin {
|
|
return (
|
|
object(value) &&
|
|
typeof value.type === 'string' &&
|
|
typeof value.target_id === 'string'
|
|
);
|
|
}
|
|
function validSections(value: unknown): value is Sections {
|
|
return (
|
|
object(value) &&
|
|
textKeys.every((key) => typeof value[key] === 'string') &&
|
|
typeof value.consent === 'boolean' &&
|
|
Array.isArray(value.mandatory) &&
|
|
value.mandatory.every((item) => typeof item === 'string') &&
|
|
Array.isArray(value.locations) &&
|
|
value.locations.every(location) &&
|
|
Array.isArray(value.origins) &&
|
|
value.origins.every(origin)
|
|
);
|
|
}
|
|
function readSnapshot(props: unknown): Snapshot | null {
|
|
if (
|
|
!object(props) ||
|
|
!Array.isArray(props.locations) ||
|
|
!Array.isArray(props.origins)
|
|
)
|
|
return null;
|
|
if (
|
|
props.editorActorId !== undefined &&
|
|
(typeof props.editorActorId !== 'number' ||
|
|
!Number.isSafeInteger(props.editorActorId) ||
|
|
props.editorActorId < 1)
|
|
)
|
|
return null;
|
|
if (
|
|
props.saveCommands !== undefined &&
|
|
(!Array.isArray(props.saveCommands) ||
|
|
!props.saveCommands.every(
|
|
(value) =>
|
|
object(value) &&
|
|
typeof value.key === 'string' &&
|
|
typeof value.version === 'number' &&
|
|
Number.isSafeInteger(value.version) &&
|
|
value.version > 0,
|
|
))
|
|
)
|
|
return null;
|
|
const record = props.record;
|
|
if (
|
|
record !== null &&
|
|
(!object(record) ||
|
|
!Number.isSafeInteger(record.id) ||
|
|
typeof record.id !== 'number' ||
|
|
record.id < 1 ||
|
|
!Number.isSafeInteger(record.version) ||
|
|
typeof record.version !== 'number' ||
|
|
record.version < 1 ||
|
|
typeof record.status !== 'string' ||
|
|
(!object(record.payload) &&
|
|
!(
|
|
Array.isArray(record.payload) && record.payload.length === 0
|
|
)))
|
|
)
|
|
return null;
|
|
const row = object(record) ? record : {};
|
|
const payload = object(row.payload) ? row.payload : {};
|
|
const values = {
|
|
title: text(row.title),
|
|
offer: text(row.offer),
|
|
seeking: text(row.seeking),
|
|
organization_id: text(row.organization_id),
|
|
sector_id:
|
|
row.sector_id == null
|
|
? ''
|
|
: typeof row.sector_id === 'number'
|
|
? String(row.sector_id)
|
|
: undefined,
|
|
kind: text(row.kind, 'seeking'),
|
|
partnership_type: text(row.partnership_type),
|
|
deadline: text(row.deadline),
|
|
summary: text(payload.summary),
|
|
requirements: text(payload.requirements),
|
|
mandatory: payload.mandatory ?? [],
|
|
confidentiality: text(payload.confidentiality, 'public'),
|
|
consent: bool(payload.consent),
|
|
availability: text(payload.availability),
|
|
locations: props.locations.map((item) =>
|
|
object(item)
|
|
? {
|
|
geo_location_id: item.geo_location_id,
|
|
role: item.role,
|
|
source_reference: text(item.source_reference),
|
|
}
|
|
: null,
|
|
),
|
|
origins: props.origins.map((item) =>
|
|
object(item)
|
|
? { type: item.type, target_id: item.target_id }
|
|
: null,
|
|
),
|
|
};
|
|
if (!validSections(values)) return null;
|
|
return {
|
|
actorId:
|
|
typeof props.editorActorId === 'number'
|
|
? props.editorActorId
|
|
: null,
|
|
commands: Array.isArray(props.saveCommands)
|
|
? props.saveCommands.map((value) => ({
|
|
key: value.key,
|
|
version: value.version,
|
|
}))
|
|
: [],
|
|
id: typeof row.id === 'number' ? row.id : null,
|
|
version: typeof row.version === 'number' ? row.version : null,
|
|
status: typeof row.status === 'string' ? row.status : 'draft',
|
|
clientKey: typeof row.client_key === 'string' ? row.client_key : null,
|
|
values,
|
|
};
|
|
}
|
|
function toDraft(
|
|
values: Sections,
|
|
client_key: string,
|
|
version: number | null,
|
|
): Draft {
|
|
const {
|
|
summary,
|
|
requirements,
|
|
mandatory,
|
|
confidentiality,
|
|
consent,
|
|
availability,
|
|
...fields
|
|
} = values;
|
|
return {
|
|
...fields,
|
|
client_key,
|
|
version,
|
|
payload: {
|
|
summary,
|
|
requirements,
|
|
mandatory,
|
|
confidentiality,
|
|
consent,
|
|
availability,
|
|
},
|
|
};
|
|
}
|
|
function sections({
|
|
payload,
|
|
client_key: _key,
|
|
version: _version,
|
|
...fields
|
|
}: Draft): Sections {
|
|
return { ...fields, ...payload };
|
|
}
|
|
function canonical(value: Sections[PartnershipSection]) {
|
|
return JSON.stringify(
|
|
Array.isArray(value)
|
|
? value.map((item) => JSON.stringify(item)).sort()
|
|
: value,
|
|
);
|
|
}
|
|
function equal(
|
|
a: Sections[PartnershipSection],
|
|
b: Sections[PartnershipSection],
|
|
) {
|
|
return canonical(a) === canonical(b);
|
|
}
|
|
function same(a: Sections, b: Sections) {
|
|
return keys.every((key) => equal(a[key], b[key]));
|
|
}
|
|
function normalised(values: Sections): Sections {
|
|
const result = { ...values };
|
|
for (const key of textKeys) result[key] = result[key].trim();
|
|
return {
|
|
...result,
|
|
locations: result.locations.map((item) => ({
|
|
geo_location_id: item.geo_location_id.trim(),
|
|
role: item.role.trim(),
|
|
source_reference: item.source_reference.trim(),
|
|
})),
|
|
origins: result.origins.map((item) => ({
|
|
type: item.type.trim(),
|
|
target_id: item.target_id.trim(),
|
|
})),
|
|
};
|
|
}
|
|
const empty = readSnapshot({ record: null, locations: [], origins: [] });
|
|
if (!empty) throw new Error('Empty partnership editor definition is invalid.');
|
|
const emptyValues = empty.values;
|
|
export function usePartnershipEdit(props: PartnershipEditProps) {
|
|
const alive = useRef(true);
|
|
useEffect(() => {
|
|
alive.current = true;
|
|
return () => {
|
|
alive.current = false;
|
|
};
|
|
}, []);
|
|
const current = readSnapshot(props);
|
|
const operationKey = useRef(createOperationKey());
|
|
const form = useForm(
|
|
toDraft(
|
|
current?.values ?? emptyValues,
|
|
operationKey.current,
|
|
current?.version ?? null,
|
|
),
|
|
);
|
|
const base = useRef(current);
|
|
const live = useRef(form.data);
|
|
live.current = form.data;
|
|
const identity = useRef(current?.id ?? null);
|
|
identity.current = current?.id ?? null;
|
|
const edited = useRef(new Set<PartnershipSection>());
|
|
const generation = useRef<Partial<Record<PartnershipSection, number>>>({});
|
|
const pendingCreation = useRef<{
|
|
command: Draft;
|
|
generation: Partial<Record<PartnershipSection, number>>;
|
|
ambiguous: boolean;
|
|
} | null>(null);
|
|
const createdId = useRef<number | null>(null);
|
|
const pendingUpdate = useRef<{
|
|
id: number;
|
|
command: Draft & { operation_key: string };
|
|
generation: Partial<Record<PartnershipSection, number>>;
|
|
base: Snapshot;
|
|
ambiguous: boolean;
|
|
} | null>(null);
|
|
const [choices, setChoices] = useState<Record<string, 'mine' | 'current'>>(
|
|
{},
|
|
);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [problem, setProblem] = useState<string | null>(null);
|
|
const [needsCompare, setNeedsCompare] = useState(false);
|
|
const [mustRefresh, setMustRefresh] = useState(false),
|
|
[accessDenied, setAccessDenied] = useState(false);
|
|
const own = sections(form.data);
|
|
const invalid =
|
|
!current ||
|
|
!base.current ||
|
|
current.actorId !== base.current.actorId ||
|
|
(current.id !== base.current.id &&
|
|
!(
|
|
base.current.id === null &&
|
|
(current.id === createdId.current ||
|
|
current.clientKey ===
|
|
pendingCreation.current?.command.client_key)
|
|
));
|
|
const stale =
|
|
!!current &&
|
|
!!base.current &&
|
|
(current.id !== base.current.id ||
|
|
current.version !== base.current.version ||
|
|
current.status !== base.current.status ||
|
|
!same(current.values, base.current.values));
|
|
const baseline = base.current;
|
|
const changed =
|
|
current && baseline
|
|
? keys.filter(
|
|
(key) => !equal(current.values[key], baseline.values[key]),
|
|
)
|
|
: [];
|
|
const conflicts = current
|
|
? changed.filter(
|
|
(key) =>
|
|
edited.current.has(key) &&
|
|
!equal(own[key], current.values[key]),
|
|
)
|
|
: [];
|
|
const choiceKey = (key: PartnershipSection) =>
|
|
JSON.stringify([
|
|
current?.id,
|
|
current?.version,
|
|
current?.status,
|
|
key,
|
|
current ? canonical(current.values[key]) : '',
|
|
]);
|
|
const unresolved = conflicts.filter(
|
|
(key) => choices[choiceKey(key)] !== 'mine',
|
|
);
|
|
const editable = !!current && editableStates.includes(current.status);
|
|
const blocked =
|
|
invalid ||
|
|
stale ||
|
|
refreshing ||
|
|
needsCompare ||
|
|
accessDenied ||
|
|
!!pendingUpdate.current ||
|
|
!editable;
|
|
const dirty =
|
|
!!baseline &&
|
|
(!!pendingUpdate.current ||
|
|
!!pendingCreation.current ||
|
|
!same(own, baseline.values) ||
|
|
(!!current &&
|
|
keys.some(
|
|
(key) =>
|
|
edited.current.has(key) &&
|
|
!equal(own[key], current.values[key]),
|
|
)));
|
|
useEffect(() => {
|
|
const leave = (event: BeforeUnloadEvent) => {
|
|
if (dirty) {
|
|
event.preventDefault();
|
|
event.returnValue = '';
|
|
}
|
|
};
|
|
const stop = router.on('before', (event) => {
|
|
if (
|
|
dirty &&
|
|
event.detail.visit.method === 'get' &&
|
|
event.detail.visit.preserveState !== true &&
|
|
!window.confirm(
|
|
'Há alterações por guardar na parceria. Pretende sair sem as guardar?',
|
|
)
|
|
)
|
|
event.preventDefault();
|
|
});
|
|
window.addEventListener('beforeunload', leave);
|
|
return () => {
|
|
stop();
|
|
window.removeEventListener('beforeunload', leave);
|
|
};
|
|
}, [dirty]);
|
|
function replace(next: Draft) {
|
|
const previous = sections(live.current),
|
|
after = sections(next);
|
|
const changed = keys.filter((key) => !equal(previous[key], after[key]));
|
|
for (const key of changed) {
|
|
edited.current.add(key);
|
|
generation.current[key] = (generation.current[key] ?? 0) + 1;
|
|
}
|
|
live.current = next;
|
|
form.setData(() => next);
|
|
setChoices((previous) => {
|
|
const next = { ...previous };
|
|
for (const key of changed) delete next[choiceKey(key)];
|
|
return next;
|
|
});
|
|
}
|
|
function setData<K extends keyof Draft>(
|
|
key: K,
|
|
value: Draft[K] | ((previous: Draft[K]) => Draft[K]),
|
|
) {
|
|
const next =
|
|
typeof value === 'function' ? value(live.current[key]) : value;
|
|
replace({ ...live.current, [key]: next });
|
|
}
|
|
function choose(key: PartnershipSection, choice: 'mine' | 'current') {
|
|
if (!current) return;
|
|
if (choice === 'current')
|
|
replace(
|
|
toDraft(
|
|
{ ...sections(live.current), [key]: current.values[key] },
|
|
live.current.client_key,
|
|
live.current.version,
|
|
),
|
|
);
|
|
setChoices((previous) => ({ ...previous, [choiceKey(key)]: choice }));
|
|
}
|
|
function adopt() {
|
|
if (
|
|
!current ||
|
|
(!stale && !pendingUpdate.current?.ambiguous && !needsCompare) ||
|
|
invalid ||
|
|
refreshing ||
|
|
form.processing ||
|
|
unresolved.length
|
|
)
|
|
return;
|
|
if (mustRefresh) return;
|
|
const selected = { ...current.values };
|
|
for (const key of keys)
|
|
if (edited.current.has(key))
|
|
Object.assign(selected, { [key]: sections(live.current)[key] });
|
|
const saved = toDraft(
|
|
current.values,
|
|
current.clientKey ?? live.current.client_key,
|
|
current.version,
|
|
);
|
|
base.current = current;
|
|
createdId.current = null;
|
|
pendingCreation.current = null;
|
|
pendingUpdate.current = null;
|
|
form.setDefaults(saved);
|
|
live.current = toDraft(selected, saved.client_key, saved.version);
|
|
form.setData(live.current);
|
|
form.clearErrors();
|
|
setProblem(null);
|
|
setNeedsCompare(false);
|
|
setChoices({});
|
|
}
|
|
function refresh() {
|
|
if (refreshing || form.processing) return;
|
|
setRefreshing(true);
|
|
router.reload({
|
|
only: [
|
|
'record',
|
|
'locations',
|
|
'origins',
|
|
'editorActorId',
|
|
'saveCommands',
|
|
],
|
|
onSuccess: (page) => {
|
|
if (!alive.current) return;
|
|
if (
|
|
pendingUpdate.current &&
|
|
acknowledgeUpdate(page.props, pendingUpdate.current)
|
|
)
|
|
return;
|
|
const fresh = readSnapshot(page.props);
|
|
if (
|
|
!fresh ||
|
|
fresh.actorId !== base.current?.actorId ||
|
|
fresh.id !== base.current?.id
|
|
) {
|
|
setProblem(
|
|
'A resposta não corresponde a esta conta e parceria. A preparação foi conservada.',
|
|
);
|
|
return;
|
|
}
|
|
setMustRefresh(false);
|
|
setAccessDenied(false);
|
|
if (
|
|
fresh &&
|
|
base.current &&
|
|
fresh.id === base.current.id &&
|
|
fresh.version === base.current.version &&
|
|
fresh.status === base.current.status &&
|
|
same(fresh.values, base.current.values)
|
|
) {
|
|
setNeedsCompare(false);
|
|
setProblem(null);
|
|
}
|
|
},
|
|
onHttpException: () => {
|
|
if (!alive.current) return false;
|
|
setAccessDenied(true);
|
|
setMustRefresh(true);
|
|
setProblem(
|
|
'Não foi possível recuperar esta parceria com o acesso actual. As suas edições foram conservadas.',
|
|
);
|
|
return false;
|
|
},
|
|
onNetworkError: () => {
|
|
if (!alive.current) return false;
|
|
setProblem(
|
|
'A recuperação não foi confirmada. As suas edições foram conservadas.',
|
|
);
|
|
return false;
|
|
},
|
|
onFinish: () => {
|
|
if (alive.current) setRefreshing(false);
|
|
},
|
|
});
|
|
}
|
|
function applySaved(
|
|
saved: Snapshot,
|
|
command: Draft,
|
|
sentGeneration: Partial<Record<PartnershipSection, number>>,
|
|
later = false,
|
|
) {
|
|
const merged = { ...saved.values };
|
|
edited.current.clear();
|
|
for (const key of keys)
|
|
if ((generation.current[key] ?? 0) !== (sentGeneration[key] ?? 0)) {
|
|
Object.assign(merged, { [key]: sections(live.current)[key] });
|
|
edited.current.add(key);
|
|
}
|
|
base.current = saved;
|
|
createdId.current = null;
|
|
pendingCreation.current = null;
|
|
pendingUpdate.current = null;
|
|
const defaults = toDraft(
|
|
saved.values,
|
|
saved.clientKey ?? command.client_key,
|
|
saved.version,
|
|
);
|
|
form.setDefaults(defaults);
|
|
live.current = toDraft(merged, defaults.client_key, defaults.version);
|
|
form.setData(live.current);
|
|
form.clearErrors();
|
|
setChoices({});
|
|
setMustRefresh(false);
|
|
setAccessDenied(false);
|
|
setNeedsCompare(later);
|
|
setProblem(
|
|
later
|
|
? 'A gravação foi reconhecida. Existem alterações posteriores; compare antes de voltar a guardar.'
|
|
: null,
|
|
);
|
|
}
|
|
function acknowledgeUpdate(
|
|
pageProps: unknown,
|
|
sent: NonNullable<typeof pendingUpdate.current>,
|
|
): boolean {
|
|
if (
|
|
!alive.current ||
|
|
pendingUpdate.current !== sent ||
|
|
identity.current !== sent.id
|
|
)
|
|
return false;
|
|
const saved = readSnapshot(pageProps),
|
|
expectedVersion = (sent.base.version ?? 0) + 1;
|
|
if (
|
|
!saved ||
|
|
saved.id !== sent.id ||
|
|
saved.actorId !== sent.base.actorId ||
|
|
!saved.commands.some(
|
|
(command) =>
|
|
command.key === sent.command.operation_key &&
|
|
command.version === expectedVersion,
|
|
) ||
|
|
saved.version === null ||
|
|
saved.version < expectedVersion
|
|
)
|
|
return false;
|
|
const expected = normalised(sections(sent.command));
|
|
if (saved.version === expectedVersion) {
|
|
if (saved.status !== 'draft' || !same(saved.values, expected))
|
|
return false;
|
|
applySaved(saved, sent.command, sent.generation);
|
|
return true;
|
|
}
|
|
applySaved(
|
|
{
|
|
...sent.base,
|
|
version: expectedVersion,
|
|
status: 'draft',
|
|
values: expected,
|
|
commands: saved.commands,
|
|
},
|
|
sent.command,
|
|
sent.generation,
|
|
true,
|
|
);
|
|
return true;
|
|
}
|
|
function save(recover = false) {
|
|
if (
|
|
!alive.current ||
|
|
form.processing ||
|
|
refreshing ||
|
|
invalid ||
|
|
accessDenied ||
|
|
(!recover && blocked)
|
|
)
|
|
return;
|
|
const update = recover ? pendingUpdate.current : null;
|
|
if (recover && (!update?.ambiguous || current?.id !== update.id))
|
|
return;
|
|
const submittedId = update?.id ?? base.current?.id ?? null;
|
|
const pending = submittedId === null ? pendingCreation.current : null;
|
|
const command =
|
|
update?.command ??
|
|
pending?.command ??
|
|
structuredClone(live.current);
|
|
const sentGeneration = update?.generation ??
|
|
pending?.generation ?? { ...generation.current };
|
|
if (submittedId !== null && !update && base.current)
|
|
pendingUpdate.current = {
|
|
id: submittedId,
|
|
command: { ...command, operation_key: createOperationKey() },
|
|
generation: sentGeneration,
|
|
base: base.current,
|
|
ambiguous: false,
|
|
};
|
|
const submittedUpdate = pendingUpdate.current;
|
|
if (submittedId === null && !pending)
|
|
pendingCreation.current = {
|
|
command,
|
|
generation: sentGeneration,
|
|
ambiguous: false,
|
|
};
|
|
setProblem(null);
|
|
form.transform(() => submittedUpdate?.command ?? command);
|
|
const options = {
|
|
preserveScroll: true,
|
|
onSuccess: (page: { props: Record<string, unknown> }) => {
|
|
if (!alive.current) return;
|
|
if (submittedUpdate) {
|
|
if (
|
|
!acknowledgeUpdate(page.props, submittedUpdate) &&
|
|
pendingUpdate.current === submittedUpdate
|
|
) {
|
|
submittedUpdate.ambiguous = true;
|
|
setMustRefresh(true);
|
|
setNeedsCompare(true);
|
|
setProblem(
|
|
'A resposta não confirmou esta gravação. A preparação foi conservada; recupere a mesma tentativa ou compare os dados actuais.',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
const saved = readSnapshot(page.props);
|
|
if (
|
|
!saved ||
|
|
saved.id === null ||
|
|
(submittedId !== null
|
|
? saved.id !== submittedId ||
|
|
identity.current !== submittedId
|
|
: saved.clientKey !== command.client_key ||
|
|
(identity.current !== null &&
|
|
identity.current !== saved.id))
|
|
) {
|
|
setProblem(
|
|
'A gravação não foi reconciliada com este formulário. Compare os dados antes de continuar.',
|
|
);
|
|
setNeedsCompare(true);
|
|
return;
|
|
}
|
|
if (submittedId === null) createdId.current = saved.id;
|
|
pendingCreation.current = null;
|
|
if (
|
|
saved.status !== 'draft' ||
|
|
!same(saved.values, normalised(sections(command)))
|
|
) {
|
|
setProblem(
|
|
'A resposta contém uma versão com dados diferentes do que enviou. Compare antes de voltar a guardar; as suas edições foram conservadas.',
|
|
);
|
|
setNeedsCompare(true);
|
|
return;
|
|
}
|
|
applySaved(saved, command, sentGeneration);
|
|
},
|
|
onError: () => {
|
|
if (!alive.current) return;
|
|
if (
|
|
submittedUpdate &&
|
|
pendingUpdate.current === submittedUpdate
|
|
)
|
|
pendingUpdate.current = null;
|
|
if (!pendingCreation.current?.ambiguous)
|
|
pendingCreation.current = null;
|
|
},
|
|
onHttpException: (response: { status: number }) => {
|
|
if (!alive.current) return false;
|
|
if (submittedUpdate) {
|
|
if (pendingUpdate.current !== submittedUpdate) return false;
|
|
if (
|
|
response.status >= 500 ||
|
|
[401, 403, 404, 419].includes(response.status)
|
|
)
|
|
submittedUpdate.ambiguous = true;
|
|
else pendingUpdate.current = null;
|
|
setMustRefresh(true);
|
|
setNeedsCompare(true);
|
|
setAccessDenied(
|
|
[401, 403, 404, 419].includes(response.status),
|
|
);
|
|
setProblem(
|
|
'A gravação não foi confirmada com os dados e acesso actuais. Compare antes de continuar; as edições foram conservadas.',
|
|
);
|
|
return false;
|
|
}
|
|
if (submittedId !== null && response.status === 409) {
|
|
setNeedsCompare(true);
|
|
setProblem(
|
|
'A parceria foi alterada ou mudou de estado. Compare com os dados actuais antes de guardar; as suas edições foram conservadas.',
|
|
);
|
|
} else {
|
|
setProblem(
|
|
response.status === 403
|
|
? 'Não foi possível confirmar a operação com o acesso actual. As suas edições foram conservadas.'
|
|
: 'A gravação não foi confirmada. As suas edições foram conservadas.',
|
|
);
|
|
if (submittedId !== null)
|
|
setNeedsCompare(response.status >= 500);
|
|
}
|
|
if (pendingCreation.current) {
|
|
if (
|
|
response.status >= 500 ||
|
|
response.status === 409 ||
|
|
pendingCreation.current.ambiguous
|
|
)
|
|
pendingCreation.current.ambiguous = true;
|
|
else pendingCreation.current = null;
|
|
}
|
|
return false;
|
|
},
|
|
onNetworkError: () => {
|
|
if (!alive.current) return false;
|
|
if (
|
|
submittedUpdate &&
|
|
pendingUpdate.current !== submittedUpdate
|
|
)
|
|
return false;
|
|
if (
|
|
submittedUpdate &&
|
|
pendingUpdate.current === submittedUpdate
|
|
) {
|
|
submittedUpdate.ambiguous = true;
|
|
setMustRefresh(true);
|
|
}
|
|
setProblem(
|
|
'A gravação não foi confirmada. As suas edições foram conservadas.',
|
|
);
|
|
if (pendingCreation.current)
|
|
pendingCreation.current.ambiguous = true;
|
|
else setNeedsCompare(true);
|
|
return false;
|
|
},
|
|
onCancel: () => {
|
|
if (!alive.current) return;
|
|
if (
|
|
submittedUpdate &&
|
|
pendingUpdate.current !== submittedUpdate
|
|
)
|
|
return;
|
|
if (
|
|
submittedUpdate &&
|
|
pendingUpdate.current === submittedUpdate
|
|
) {
|
|
submittedUpdate.ambiguous = true;
|
|
setMustRefresh(true);
|
|
}
|
|
if (pendingCreation.current)
|
|
pendingCreation.current.ambiguous = true;
|
|
else setNeedsCompare(true);
|
|
setProblem(
|
|
'A gravação foi interrompida; conserve a edição e recupere os dados antes de continuar.',
|
|
);
|
|
},
|
|
};
|
|
if (submittedId === null) form.post('/parcerias/rascunhos', options);
|
|
else form.put(`/parcerias/${submittedId}`, options);
|
|
}
|
|
return {
|
|
form,
|
|
setData,
|
|
save,
|
|
refresh,
|
|
adopt,
|
|
choose,
|
|
problem,
|
|
current,
|
|
values: own,
|
|
changed,
|
|
conflicts,
|
|
unresolved,
|
|
stale,
|
|
invalid,
|
|
refreshing,
|
|
editable,
|
|
blocked,
|
|
dirty,
|
|
retryingCreation: !!pendingCreation.current?.ambiguous,
|
|
retryingSave: !!pendingUpdate.current?.ambiguous,
|
|
canRecoverSave:
|
|
!!pendingUpdate.current?.ambiguous &&
|
|
!invalid &&
|
|
!accessDenied &&
|
|
!refreshing &&
|
|
!form.processing,
|
|
recoverSave: () => save(true),
|
|
mustRefresh,
|
|
baseVersion: base.current?.version ?? null,
|
|
};
|
|
}
|
|
export type PartnershipEditor = ReturnType<typeof usePartnershipEdit>;
|