mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-03 07:23:40 +00:00
fix: export colmn
This commit is contained in:
@@ -122,6 +122,16 @@ export const customersDataset: ExportDataset = {
|
||||
{ value: 'ethiopian', label: 'Ethiopian' },
|
||||
{ value: 'foreign', label: 'Foreign' },
|
||||
] },
|
||||
// The operational role, NOT `type` above — the list's Role pill. One
|
||||
// `customer` company routinely holds several profiles, so this asks "who
|
||||
// does X?" rather than "what kind of company is this?".
|
||||
{ key: 'profileType', label: 'Role', type: 'select', options: [
|
||||
{ value: 'importer', label: 'Importer' },
|
||||
{ value: 'exporter', label: 'Exporter' },
|
||||
{ value: 'freight_forwarder', label: 'Freight forwarder' },
|
||||
{ value: 'dj_freight_forwarder', label: 'DJ freight forwarder' },
|
||||
{ value: 'transporter', label: 'Transporter' },
|
||||
] },
|
||||
// The list's Status filter folds the review queues in, and sends these two
|
||||
// alongside `status`. They are predicates, not columns — see
|
||||
// `company-scope.sql.ts`, shared with the list so both agree exactly.
|
||||
@@ -147,6 +157,18 @@ export const customersDataset: ExportDataset = {
|
||||
if (params.kind) qb.andWhere('c.kind = :kind', { kind: params.kind });
|
||||
if (params.status) qb.andWhere('c.status = :status', { status: params.status });
|
||||
if (params.nationality) qb.andWhere('c.nationality = :nationality', { nationality: params.nationality });
|
||||
if (params.profileType) {
|
||||
// EXISTS, matching the list repository exactly — a join here would
|
||||
// multiply a company holding two profiles into two rows and put the file
|
||||
// out of step with the count endpoint.
|
||||
qb.andWhere(
|
||||
`EXISTS (SELECT 1 FROM freight.company_profiles cp_type
|
||||
WHERE cp_type.company_id = c.id
|
||||
AND cp_type.deleted_at IS NULL
|
||||
AND cp_type.type = :profileType)`,
|
||||
{ profileType: params.profileType },
|
||||
);
|
||||
}
|
||||
if (params.onboardingCompleted) {
|
||||
const draft = companyDraftSql('c');
|
||||
qb.andWhere(params.onboardingCompleted === 'true' ? `NOT ${draft}` : draft);
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
EXPORT_MIME,
|
||||
formatRowCap,
|
||||
pickByKey,
|
||||
pickDatasetFields,
|
||||
resolveExportFormat,
|
||||
resolveRowLimit,
|
||||
} from './export-request.util';
|
||||
@@ -85,3 +86,48 @@ describe('pickByKey', () => {
|
||||
expect(pickByKey(columns, 'ghost,also-ghost')).toEqual(columns);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pickDatasetFields', () => {
|
||||
const fields = [
|
||||
{ key: 'name', label: 'Company', default: true },
|
||||
{ key: 'tin', label: 'TIN', default: true },
|
||||
{ key: 'website', label: 'Website' },
|
||||
{ key: 'kebele', label: 'Kebele' },
|
||||
];
|
||||
const defaults = [fields[0], fields[1]];
|
||||
|
||||
it.each([undefined, '', ' ', ','])('%p means the default fields', (raw) => {
|
||||
expect(pickDatasetFields(fields, raw)).toEqual(defaults);
|
||||
});
|
||||
|
||||
it('a subset is honoured, in the dataset\'s own order', () => {
|
||||
expect(pickDatasetFields(fields, 'kebele,name')).toEqual([fields[0], fields[3]]);
|
||||
});
|
||||
|
||||
it('asking for EVERY field exports every field', () => {
|
||||
// The dialog's "All columns" chip sends exactly this. Falling back to the
|
||||
// defaults here was the bug: 37 ticked customer columns exported as 9.
|
||||
expect(pickDatasetFields(fields, 'name,tin,website,kebele')).toEqual(fields);
|
||||
});
|
||||
|
||||
it('a non-default field alone is not widened back to the defaults', () => {
|
||||
expect(pickDatasetFields(fields, 'website')).toEqual([fields[2]]);
|
||||
});
|
||||
|
||||
it('unknown keys are dropped, the recognised ones still stand', () => {
|
||||
expect(pickDatasetFields(fields, 'ghost,website')).toEqual([fields[2]]);
|
||||
});
|
||||
|
||||
it('all-unknown keys fall back to the defaults, not to everything', () => {
|
||||
expect(pickDatasetFields(fields, 'ghost,also-ghost')).toEqual(defaults);
|
||||
});
|
||||
|
||||
it('surrounding whitespace in a hand-built fields list is tolerated', () => {
|
||||
expect(pickDatasetFields(fields, ' name , website ')).toEqual([fields[0], fields[2]]);
|
||||
});
|
||||
|
||||
it('a dataset with no default flags falls back to every field', () => {
|
||||
const flat = [{ key: 'a', label: 'A' }, { key: 'b', label: 'B' }];
|
||||
expect(pickDatasetFields(flat, undefined)).toEqual(flat);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,3 +56,31 @@ export function pickByKey<T extends { key: string }>(all: T[], raw: string | und
|
||||
const filtered = requested?.length ? all.filter((c) => requested.includes(c.key)) : all;
|
||||
return filtered.length ? filtered : all;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dataset's requested field subset, whitelisted against what the caller may
|
||||
* have. Unlike `pickByKey`, "nothing recognised" falls back to the DEFAULT
|
||||
* fields rather than to every field — a bookings export declares ~70 columns
|
||||
* and dumping all of them on an unparameterised call is nobody's intent.
|
||||
*
|
||||
* Selecting every field is a legitimate request — the dialog's "All columns"
|
||||
* chip sends exactly that — so the fallback keys off whether any requested key
|
||||
* MATCHED, never off how many fields came back. Comparing the picked count to
|
||||
* `all.length` (as this did originally) made "All columns" silently export the
|
||||
* default columns instead.
|
||||
*/
|
||||
export function pickDatasetFields<T extends { key: string; default?: boolean }>(
|
||||
all: T[],
|
||||
raw: string | undefined,
|
||||
): T[] {
|
||||
const requested = new Set(
|
||||
raw
|
||||
?.split(',')
|
||||
.map((k) => k.trim())
|
||||
.filter(Boolean) ?? [],
|
||||
);
|
||||
const picked = requested.size ? all.filter((f) => requested.has(f.key)) : [];
|
||||
if (picked.length) return picked;
|
||||
const defaults = all.filter((f) => f.default);
|
||||
return defaults.length ? defaults : all;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { resolveDatasetFields, resolveFilterOptions } from './export-filter.util
|
||||
import {
|
||||
EXPORT_MIME,
|
||||
formatRowCap,
|
||||
pickByKey,
|
||||
pickDatasetFields,
|
||||
resolveExportFormat,
|
||||
resolveRowLimit,
|
||||
} from './export-request.util';
|
||||
@@ -105,7 +105,7 @@ export class ExportsController {
|
||||
const dataset = this.resolve(key, user);
|
||||
const directions = await this.userTradeAccessService.resolveAllowedDirections(user);
|
||||
const format = resolveExportFormat(query.format);
|
||||
const fields = ExportsController.pickFields(
|
||||
const fields = pickDatasetFields(
|
||||
await resolveDatasetFields(dataset, this.dataSource),
|
||||
query.fields,
|
||||
);
|
||||
@@ -135,22 +135,6 @@ export class ExportsController {
|
||||
res.send(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requested fields, whitelisted against the dataset. No `fields=` means the
|
||||
* DEFAULT set, not everything — a booking export has ~70 fields and dumping
|
||||
* all of them on an unparameterised call is nobody's intent.
|
||||
*/
|
||||
private static pickFields(all: ExportField[], raw: string | undefined): ExportField[] {
|
||||
if (raw?.trim()) {
|
||||
const picked = pickByKey(all, raw);
|
||||
// pickByKey falls back to everything when nothing matched; for a dataset
|
||||
// the safer read of "all keys unknown" is still the default set.
|
||||
if (picked.length !== all.length) return picked;
|
||||
}
|
||||
const defaults = all.filter((f) => f.default);
|
||||
return defaults.length ? defaults : all;
|
||||
}
|
||||
|
||||
private resolve(key: string, user: TCurrentUser): ExportDataset {
|
||||
const dataset = getDataset(key);
|
||||
if (!dataset) throw new NotFoundException(`Unknown export dataset: ${key}`);
|
||||
|
||||
Reference in New Issue
Block a user