Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 7 additions & 85 deletions website/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -390,91 +390,6 @@ body[data-scrolled='true'] .navbar {
display: none !important;
}

/* Keep invoice mode and related fields together when requirements change. */
[id='operation/createInvoice'] tbody:has(> tr > td[title='status'] + td select) {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);

> tr {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
}

> tr > td {
width: auto;
}

> tr > td[colspan] {
grid-column: 1 / -1;
}

> tr:has(> td[colspan]) {
animation: invoice-field-details-enter 180ms ease-out;
transform-origin: top;
}

> tr:has(> td[title='status']),
> tr:has(> td[title='status']) + tr:has(> td[colspan]) {
order: -11;
}

> tr:has(> td[title='status']) {
> td + td > div {
display: flex;
flex-direction: column;

> div:has(> select) {
order: -1;
margin-bottom: 0.5rem;
}
}
}

> tr:has(> td[title='type']),
> tr:has(> td[title='type']) + tr:has(> td[colspan]) { order: -10; }
> tr:has(> td[title='customer']),
> tr:has(> td[title='customer']) + tr:has(> td[colspan]) { order: -9; }
> tr:has(> td[title='third_party']),
> tr:has(> td[title='third_party']) + tr:has(> td[colspan]) { order: -8; }
> tr:has(> td:is([title='items'], [title='complements'])),
> tr:has(> td:is([title='items'], [title='complements'])) + tr:has(> td[colspan]) { order: -7; }
> tr:has(> td[title='use']),
> tr:has(> td[title='use']) + tr:has(> td[colspan]) { order: -6; }
> tr:has(> td:is([title='payment_form'], [title='payment_method'], [title='conditions'])),
> tr:has(> td:is([title='payment_form'], [title='payment_method'], [title='conditions'])) + tr:has(> td[colspan]) { order: -5; }
> tr:has(> td:is([title='currency'], [title='exchange'])),
> tr:has(> td:is([title='currency'], [title='exchange'])) + tr:has(> td[colspan]) { order: -4; }
> tr:has(> td:is([title='date'], [title='folio_number'], [title='series'])),
> tr:has(> td:is([title='date'], [title='folio_number'], [title='series'])) + tr:has(> td[colspan]) { order: -3; }
> tr:has(> td:is([title='related_documents'], [title='global'], [title='export'])),
> tr:has(> td:is([title='related_documents'], [title='global'], [title='export'])) + tr:has(> td[colspan]) { order: 1; }
> tr:has(> td:is([title='pdf_custom_section'], [title='addenda'], [title='namespaces'], [title='pdf_options'])),
> tr:has(> td:is([title='pdf_custom_section'], [title='addenda'], [title='namespaces'], [title='pdf_options'])) + tr:has(> td[colspan]) { order: 2; }

@media (max-width: 50rem) {
grid-template-columns: minmax(0, 1fr);
}
}

@keyframes invoice-field-details-enter {
from {
opacity: 0;
transform: translateY(-0.4rem);
}

to {
opacity: 1;
transform: translateY(0);
}
}

@media (prefers-reduced-motion: reduce) {
[id='operation/createInvoice'] tbody:has(> tr > td[title='status'] + td select) > tr:has(> td[colspan]) {
animation: none;
}
}

html[data-theme='dark'] .redocusaurus button[aria-expanded='true'] {
border-color: transparent !important;
}
Expand Down Expand Up @@ -532,3 +447,10 @@ html[data-theme='dark'] .redocusaurus div[aria-hidden='false'] [tabindex='0'] {
[id='operation/createInvoice'] tr:has(> td[title='customer']) + tr > td > div > div > div > span:first-child {
display: none;
}

html[data-theme='dark'] .redocusaurus select,
html[data-theme='dark'] .redocusaurus select option {
color: var(--ifm-font-color-base);
background-color: var(--facturapi-surface-bg);
color-scheme: dark;
}
126 changes: 109 additions & 17 deletions website/src/pages/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,118 @@ function CustomPage() {
? 'Datos del cliente o ID de un cliente registrado.'
: 'Customer details or the ID of a registered customer.',
};
// ReDoc needs named discriminator variants to render a status selector.
// Keep this presentation hint out of the contract: status may be omitted.
spec.components.schemas.InvoiceDraftCommonInput.allOf[1].properties.customer = {
...spec.components.schemas.InvoiceCommonInput.allOf[0].properties.customer,
oneOf: [
...spec.components.schemas.InvoiceCustomerInput.oneOf,
{
type: 'null',
title: locale === 'es' ? 'Sin cliente' : 'No customer',
},
],
};
for (const customer of [
spec.components.schemas.ReceiptInput.allOf[0].properties.customer,
spec.components.schemas.ReceiptAssignCustomerInput.properties.customer,
spec.components.schemas.InvoiceReceiptInput.allOf[0].properties.customer,
spec.components.schemas.ToInvoiceInput.properties.customer,
spec.components.schemas.ToInvoicePreviewInput.properties.customer,
spec.components.schemas.RetentionUpdateInput.properties.customer,
]) {
customer.oneOf = customer.oneOf.map((variant: {$ref?: string; type?: string}) => {
if (variant.$ref === '#/components/schemas/CustomerCreateInput') {
return spec.components.schemas.InvoiceCustomerInput.oneOf[0];
}
return {
...variant,
title: variant.type === 'null'
? (locale === 'es' ? 'Sin cliente' : 'No customer')
: (locale === 'es' ? 'Cliente registrado' : 'Registered customer'),
};
});
}
for (const name of ['InvoiceItemInput', 'InvoiceItemEgresoInput', 'InvoiceItemTrasladoInput']) {
const product = spec.components.schemas[name].properties.product;
product.description = locale === 'es'
? 'Datos del producto o servicio, o ID de un producto registrado.'
: 'Product or service details, or the ID of a registered product.';
product.oneOf = product.oneOf.map((variant: {$ref?: string; type?: string}) => ({
...variant,
title: variant.type === 'string'
? (locale === 'es' ? 'Producto registrado' : 'Registered product')
: (locale === 'es' ? 'Datos del producto' : 'Product details'),
}));
}
// Native oneOf tabs keep the viewing mode separate from request fields.
spec.components.schemas.InvoiceCreateInput.oneOf =
spec.components.schemas.InvoiceCreateInput.oneOf.map(
(invoiceType: {oneOf: {allOf: {properties: {status: {const: string}}}[]}[]}, typeIndex: number) => {
const mapping: Record<string, string> = {};
return {
...invoiceType,
oneOf: invoiceType.oneOf.map((variant) => {
const status = variant.allOf.at(-1)!.properties.status.const;
const name = `InvoiceCreateDisplay${typeIndex}${status}`;
spec.components.schemas[name] = variant;
mapping[status] = `#/components/schemas/${name}`;
return {$ref: mapping[status]};
}),
discriminator: {propertyName: 'status', mapping},
};
},
(invoiceType: {oneOf: {title: string}[]}) => ({
...invoiceType,
oneOf: invoiceType.oneOf.map((variant) => ({
...variant,
title: variant.title === 'draft'
? (locale === 'es' ? 'Borrador' : 'Draft')
: (locale === 'es' ? 'Timbrar' : 'Issue'),
})),
}),
);
return {...specData, spec};
for (const [name, schema] of Object.entries<{allOf?: {properties?: {type?: {description: string}}}[]; properties?: {type?: {description: string}}}>(spec.components.schemas)) {
if (!name.startsWith('Invoice') || !name.endsWith('Input')) continue;
for (const part of [schema, ...(schema.allOf || [])]) {
if (part.properties?.type) {
part.properties.type.description = locale === 'es'
? 'Código del tipo de factura seleccionado arriba. Selecciona otro tipo para consultar sus campos y código correspondiente.'
: 'Code for the invoice type selected above. Select another type to see its fields and corresponding code.';
}
}
}
type OrderedSchema = {
$ref?: string;
allOf?: OrderedSchema[];
properties?: Record<string, object>;
};
function propertyNames(schema: OrderedSchema): string[] {
if (schema.$ref) {
return propertyNames(spec.components.schemas[schema.$ref.split('/').pop()!]);
}
return [
...Object.keys(schema.properties || {}),
...(schema.allOf || []).flatMap(propertyNames),
];
}
const orderedInputs: [OrderedSchema[], string[]][] = [
[
spec.components.schemas.InvoiceCreateInput.oneOf.flatMap(
(invoiceType: {oneOf: OrderedSchema[]}) => invoiceType.oneOf,
),
['customer', 'items', 'payment_form', 'type', 'status', 'payment_method',
'use', 'currency', 'exchange', 'conditions', 'date', 'folio_number',
'series', 'address', 'third_party', 'related_documents', 'global',
'export', 'complements', 'external_id', 'idempotency_key',
'pdf_custom_section', 'addenda', 'namespaces', 'pdf_options'],
],
[
spec.components.schemas.RetentionInput.oneOf,
['customer', 'cve_retenc', 'periodo', 'totales', 'status', 'fecha_exp',
'desc_retenc', 'folio_int', 'external_id', 'idempotency_key',
'complements', 'pdf_custom_section', 'addenda', 'namespaces'],
],
];
for (const [variants, order] of orderedInputs) {
for (const variant of variants) {
const names = new Set(propertyNames(variant));
// Empty schemas establish insertion order; allOf keeps every field constraint.
variant.properties = Object.fromEntries([
...order.filter((name) => names.has(name)),
...[...names].filter((name) => !order.includes(name)),
].map((name) => [name, variant.properties?.[name] || {}]));
}
}
return {
...specData,
spec,
optionsOverrides: {sortRequiredPropsFirst: false},
};
}, [specData, locale]);
return (
<ApiDoc
Expand Down
Loading