mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat(base): add csv cell serializer with per-type rules
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { serializeCellForCsv } from './cell-csv-serializer';
|
||||
import { BasePropertyType } from '../base.schemas';
|
||||
|
||||
const p = (type: string, typeOptions: unknown = {}) => ({
|
||||
id: 'prop-1',
|
||||
type: type as any,
|
||||
typeOptions,
|
||||
});
|
||||
|
||||
describe('serializeCellForCsv', () => {
|
||||
const userNames = new Map([
|
||||
['u1', 'Alice'],
|
||||
['u2', 'Bob'],
|
||||
]);
|
||||
|
||||
it('returns empty string for null/undefined', () => {
|
||||
expect(serializeCellForCsv(p(BasePropertyType.TEXT), null, {})).toBe('');
|
||||
expect(serializeCellForCsv(p(BasePropertyType.NUMBER), undefined, {})).toBe('');
|
||||
});
|
||||
|
||||
it('stringifies text/url/email as-is', () => {
|
||||
expect(serializeCellForCsv(p(BasePropertyType.TEXT), 'hi', {})).toBe('hi');
|
||||
expect(serializeCellForCsv(p(BasePropertyType.URL), 'https://x', {})).toBe('https://x');
|
||||
expect(serializeCellForCsv(p(BasePropertyType.EMAIL), 'a@b.com', {})).toBe('a@b.com');
|
||||
});
|
||||
|
||||
it('stringifies number', () => {
|
||||
expect(serializeCellForCsv(p(BasePropertyType.NUMBER), 42, {})).toBe('42');
|
||||
expect(serializeCellForCsv(p(BasePropertyType.NUMBER), 0, {})).toBe('0');
|
||||
});
|
||||
|
||||
it('renders checkbox as true/false', () => {
|
||||
expect(serializeCellForCsv(p(BasePropertyType.CHECKBOX), true, {})).toBe('true');
|
||||
expect(serializeCellForCsv(p(BasePropertyType.CHECKBOX), false, {})).toBe('false');
|
||||
});
|
||||
|
||||
it('resolves select/status choice name', () => {
|
||||
const prop = p(BasePropertyType.SELECT, {
|
||||
choices: [
|
||||
{ id: 'c1', name: 'Red', color: 'red' },
|
||||
{ id: 'c2', name: 'Green', color: 'green' },
|
||||
],
|
||||
});
|
||||
expect(serializeCellForCsv(prop, 'c1', {})).toBe('Red');
|
||||
expect(serializeCellForCsv(prop, 'unknown', {})).toBe('');
|
||||
});
|
||||
|
||||
it('joins multiSelect names with "; " preserving order', () => {
|
||||
const prop = p(BasePropertyType.MULTI_SELECT, {
|
||||
choices: [
|
||||
{ id: 'c1', name: 'A', color: 'red' },
|
||||
{ id: 'c2', name: 'B', color: 'blue' },
|
||||
],
|
||||
});
|
||||
expect(serializeCellForCsv(prop, ['c2', 'c1'], {})).toBe('B; A');
|
||||
});
|
||||
|
||||
it('resolves person scalar and array', () => {
|
||||
const prop = p(BasePropertyType.PERSON);
|
||||
expect(serializeCellForCsv(prop, 'u1', { userNames })).toBe('Alice');
|
||||
expect(serializeCellForCsv(prop, ['u1', 'u2', 'missing'], { userNames })).toBe(
|
||||
'Alice; Bob',
|
||||
);
|
||||
});
|
||||
|
||||
it('joins file names from cell payload', () => {
|
||||
const prop = p(BasePropertyType.FILE);
|
||||
expect(
|
||||
serializeCellForCsv(
|
||||
prop,
|
||||
[
|
||||
{ id: 'f1', fileName: 'a.pdf' },
|
||||
{ id: 'f2', fileName: 'b.png' },
|
||||
],
|
||||
{},
|
||||
),
|
||||
).toBe('a.pdf; b.png');
|
||||
});
|
||||
|
||||
it('dates pass through as ISO strings', () => {
|
||||
const iso = '2026-04-18T12:00:00.000Z';
|
||||
expect(serializeCellForCsv(p(BasePropertyType.DATE), iso, {})).toBe(iso);
|
||||
});
|
||||
|
||||
it('lastEditedBy resolves via userNames', () => {
|
||||
const prop = p(BasePropertyType.LAST_EDITED_BY);
|
||||
expect(serializeCellForCsv(prop, 'u2', { userNames })).toBe('Bob');
|
||||
expect(serializeCellForCsv(prop, 'missing', { userNames })).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { BasePropertyType, BasePropertyTypeValue } from '../base.schemas';
|
||||
|
||||
export type CellCsvContext = {
|
||||
userNames?: Map<string, string>;
|
||||
};
|
||||
|
||||
type PropertyLike = {
|
||||
id: string;
|
||||
type: BasePropertyTypeValue | string;
|
||||
typeOptions?: unknown;
|
||||
};
|
||||
|
||||
function resolveChoiceName(typeOptions: unknown, id: unknown): string {
|
||||
if (!typeOptions || typeof typeOptions !== 'object') return '';
|
||||
const choices = (typeOptions as any).choices;
|
||||
if (!Array.isArray(choices)) return '';
|
||||
const match = choices.find((c: any) => c?.id === id);
|
||||
return typeof match?.name === 'string' ? match.name : '';
|
||||
}
|
||||
|
||||
function resolveUser(id: unknown, ctx: CellCsvContext): string {
|
||||
if (typeof id !== 'string') return '';
|
||||
return ctx.userNames?.get(id) ?? '';
|
||||
}
|
||||
|
||||
export function serializeCellForCsv(
|
||||
property: PropertyLike,
|
||||
value: unknown,
|
||||
ctx: CellCsvContext,
|
||||
): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
|
||||
switch (property.type) {
|
||||
case BasePropertyType.TEXT:
|
||||
case BasePropertyType.URL:
|
||||
case BasePropertyType.EMAIL:
|
||||
return String(value);
|
||||
|
||||
case BasePropertyType.NUMBER:
|
||||
return typeof value === 'number' ? String(value) : String(value ?? '');
|
||||
|
||||
case BasePropertyType.CHECKBOX:
|
||||
return value === true ? 'true' : 'false';
|
||||
|
||||
case BasePropertyType.DATE:
|
||||
case BasePropertyType.CREATED_AT:
|
||||
case BasePropertyType.LAST_EDITED_AT:
|
||||
if (value instanceof Date) return value.toISOString();
|
||||
return String(value);
|
||||
|
||||
case BasePropertyType.SELECT:
|
||||
case BasePropertyType.STATUS:
|
||||
return resolveChoiceName(property.typeOptions, value);
|
||||
|
||||
case BasePropertyType.MULTI_SELECT:
|
||||
if (!Array.isArray(value)) return '';
|
||||
return value
|
||||
.map((v) => resolveChoiceName(property.typeOptions, v))
|
||||
.filter((s) => s.length > 0)
|
||||
.join('; ');
|
||||
|
||||
case BasePropertyType.PERSON: {
|
||||
const ids = Array.isArray(value) ? value : [value];
|
||||
return ids
|
||||
.map((id) => resolveUser(id, ctx))
|
||||
.filter((s) => s.length > 0)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
case BasePropertyType.FILE:
|
||||
if (!Array.isArray(value)) return '';
|
||||
return value
|
||||
.map((f: any) =>
|
||||
f && typeof f === 'object' && typeof f.fileName === 'string'
|
||||
? f.fileName
|
||||
: '',
|
||||
)
|
||||
.filter((s) => s.length > 0)
|
||||
.join('; ');
|
||||
|
||||
case BasePropertyType.LAST_EDITED_BY:
|
||||
return resolveUser(value, ctx);
|
||||
|
||||
default:
|
||||
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user