diff --git a/apps/papra-client/src/modules/demo/demo-api-mock.ts b/apps/papra-client/src/modules/demo/demo-api-mock.ts index f4900a9c..94cb64f9 100644 --- a/apps/papra-client/src/modules/demo/demo-api-mock.ts +++ b/apps/papra-client/src/modules/demo/demo-api-mock.ts @@ -1,7 +1,9 @@ import type { ApiKey } from '../api-keys/api-keys.types'; +import type { CustomPropertyDefinition } from '../custom-properties/custom-properties.types'; import type { Document } from '../documents/documents.types'; import type { Webhook } from '../webhooks/webhooks.types'; import type { + DocumentCustomPropertyValueStorage, DocumentFile, } from './demo.storage'; import { FetchError } from 'ofetch'; @@ -11,6 +13,8 @@ import { defineHandler } from './demo-api-mock.models'; import { createId, randomString } from './demo.models'; import { apiKeyStorage, + customPropertyDefinitionStorage, + documentCustomPropertyValueStorage, documentFileStorage, documentStorage, organizationStorage, @@ -65,6 +69,61 @@ async function deserializeFile(storageInfo: DocumentFile): Promise { return new File([await fromBase64(base64Content)], name, { type }); } +function hydratePropertyValue({ value, definition }: { value: unknown; definition: CustomPropertyDefinition }): unknown { + if (value == null) { + return null; + } + + if (definition.type === 'select') { + const optionId = String(value); + const option = definition.options.find(o => o.id === optionId || o.key === optionId); + + if (!option) { + return null; + } + + return { optionId: option.id, name: option.name }; + } + + if (definition.type === 'multi_select') { + const ids = Array.isArray(value) ? value.map(String) : [String(value)]; + + return ids + .map((id) => { + const option = definition.options.find(o => o.id === id || o.key === id); + + return option ? { optionId: option.id, name: option.name } : null; + }) + .filter(v => v !== null); + } + + return value; +} + +function buildCustomPropertiesResponse({ + definitions, + storedValues, + documentId, +}: { + definitions: CustomPropertyDefinition[]; + storedValues: DocumentCustomPropertyValueStorage[]; + documentId: string; +}) { + const documentValues = storedValues.filter(v => v.documentId === documentId); + const valuesByDefinitionId = Object.fromEntries(documentValues.map(v => [v.propertyDefinitionId, v.value])); + + return definitions + .toSorted((a, b) => a.displayOrder - b.displayOrder) + .map(def => ({ + propertyDefinitionId: def.id, + key: def.key, + name: def.name, + type: def.type, + displayOrder: def.displayOrder, + value: hydratePropertyValue({ value: valuesByDefinitionId[def.id] ?? null, definition: def }), + })); +} + const inMemoryApiMock: Record = { ...defineHandler({ path: '/api/config', @@ -166,23 +225,32 @@ const inMemoryApiMock: Record = { assert(organization, { status: 403 }); const searchQuery = rawSearchQuery.trim(); - const [organizationDocuments, allTags, tagDocuments] = await Promise.all([ + const [organizationDocuments, allTags, tagDocuments, allDefinitions, allPropertyValues] = await Promise.all([ findMany(documentStorage, document => document?.organizationId === organizationId && !document?.deletedAt), getValues(tagStorage), getValues(tagDocumentStorage), + findMany(customPropertyDefinitionStorage, def => def.organizationId === organizationId), + getValues(documentCustomPropertyValueStorage), ]); - const documentsWithTags = organizationDocuments.map((document) => { + const documentsWithTagsAndProperties = organizationDocuments.map((document) => { const documentTagDocuments = tagDocuments.filter(tagDocument => tagDocument?.documentId === document?.id); const tags = allTags.filter(tag => documentTagDocuments.some(tagDocument => tagDocument?.tagId === tag?.id)); + const customProperties = buildCustomPropertiesResponse({ + definitions: allDefinitions, + storedValues: allPropertyValues, + documentId: document.id, + }); + return { ...document, tags, + customProperties, }; }); - const filteredDocuments = searchDemoDocuments({ query: searchQuery, documents: documentsWithTags as Document[] }); + const filteredDocuments = searchDemoDocuments({ query: searchQuery, documents: documentsWithTagsAndProperties as unknown as Document[] }); const paginatedDocuments = filteredDocuments .toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) @@ -223,13 +291,25 @@ const inMemoryApiMock: Record = { assert(document, { status: 404 }); - const tagDocuments = await findMany(tagDocumentStorage, tagDocument => tagDocument.documentId === documentId); + const [tagDocuments, allDefinitions, allPropertyValues] = await Promise.all([ + findMany(tagDocumentStorage, tagDocument => tagDocument.documentId === documentId), + findMany(customPropertyDefinitionStorage, def => def.organizationId === organizationId), + findMany(documentCustomPropertyValueStorage, v => v.documentId === documentId), + ]); + const tags = await findMany(tagStorage, tag => tagDocuments.some(tagDocument => tagDocument.tagId === tag.id)); + const customProperties = buildCustomPropertiesResponse({ + definitions: allDefinitions, + storedValues: allPropertyValues, + documentId, + }); + return { document: { ...document, tags, + customProperties, }, }; }, @@ -862,6 +942,173 @@ const inMemoryApiMock: Record = { }; }, }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/custom-properties', + method: 'GET', + handler: async ({ params: { organizationId } }) => { + const organization = await organizationStorage.getItem(organizationId); + + assert(organization, { status: 403 }); + + const propertyDefinitions = await findMany(customPropertyDefinitionStorage, def => def.organizationId === organizationId); + + return { propertyDefinitions }; + }, + }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/custom-properties', + method: 'POST', + handler: async ({ params: { organizationId }, body }) => { + const organization = await organizationStorage.getItem(organizationId); + + assert(organization, { status: 403 }); + + const existingDefinitions = await findMany(customPropertyDefinitionStorage, def => def.organizationId === organizationId); + + const propertyDefinition = { + id: createId({ prefix: 'cpd' }), + organizationId, + name: get(body, ['name']) as string, + key: (get(body, ['name']) as string).toLowerCase().replace(/\s+/g, '_'), + description: (get(body, ['description']) ?? null) as string | null, + type: get(body, ['type']) as string, + displayOrder: existingDefinitions.length, + options: (get(body, ['options']) as { name: string }[] ?? []).map((option, index) => ({ + id: createId({ prefix: 'opt' }), + name: option.name, + key: option.name.toLowerCase().replace(/\s+/g, '_'), + displayOrder: index, + })), + createdAt: new Date(), + updatedAt: new Date(), + }; + + await customPropertyDefinitionStorage.setItem(propertyDefinition.id, propertyDefinition as any); + + return { propertyDefinition }; + }, + }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/custom-properties/:propertyDefinitionId', + method: 'GET', + handler: async ({ params: { organizationId, propertyDefinitionId } }) => { + const organization = await organizationStorage.getItem(organizationId); + + assert(organization, { status: 403 }); + + const definition = await customPropertyDefinitionStorage.getItem(propertyDefinitionId); + + assert(definition, { status: 404 }); + + return { definition }; + }, + }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/custom-properties/:propertyDefinitionId', + method: 'PUT', + handler: async ({ params: { organizationId, propertyDefinitionId }, body }) => { + const organization = await organizationStorage.getItem(organizationId); + + assert(organization, { status: 403 }); + + const propertyDefinition = await customPropertyDefinitionStorage.getItem(propertyDefinitionId); + + assert(propertyDefinition, { status: 404 }); + + const updatedDefinition = Object.assign(propertyDefinition, body, { updatedAt: new Date() }); + + await customPropertyDefinitionStorage.setItem(propertyDefinitionId, updatedDefinition); + + return { propertyDefinition: updatedDefinition }; + }, + }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/custom-properties/:propertyDefinitionId', + method: 'DELETE', + handler: async ({ params: { organizationId, propertyDefinitionId } }) => { + const organization = await organizationStorage.getItem(organizationId); + + assert(organization, { status: 403 }); + + await customPropertyDefinitionStorage.removeItem(propertyDefinitionId); + + // Remove all values associated with this definition + const values = await findMany(documentCustomPropertyValueStorage, v => v.propertyDefinitionId === propertyDefinitionId); + + await Promise.all(values.map(v => documentCustomPropertyValueStorage.removeItem(`${v.documentId}:${propertyDefinitionId}`))); + }, + }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/documents/:documentId/custom-properties', + method: 'GET', + handler: async ({ params: { organizationId, documentId } }) => { + const key = `${organizationId}:${documentId}`; + const document = await documentStorage.getItem(key); + + assert(document, { status: 404 }); + + const [allDefinitions, allValues] = await Promise.all([ + findMany(customPropertyDefinitionStorage, def => def.organizationId === organizationId), + findMany(documentCustomPropertyValueStorage, v => v.documentId === documentId), + ]); + + const customProperties = buildCustomPropertiesResponse({ + definitions: allDefinitions, + storedValues: allValues, + documentId, + }); + + return { customProperties }; + }, + }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/documents/:documentId/custom-properties/:propertyDefinitionId', + method: 'PUT', + handler: async ({ params: { organizationId, documentId, propertyDefinitionId }, body }) => { + const docKey = `${organizationId}:${documentId}`; + const document = await documentStorage.getItem(docKey); + + assert(document, { status: 404 }); + + const definition = await customPropertyDefinitionStorage.getItem(propertyDefinitionId); + + assert(definition, { status: 404 }); + + const valueKey = `${documentId}:${propertyDefinitionId}`; + const existing = await documentCustomPropertyValueStorage.getItem(valueKey); + + const value = get(body, ['value']); + + await documentCustomPropertyValueStorage.setItem(valueKey, { + id: existing?.id ?? createId({ prefix: 'dcpv' }), + documentId, + propertyDefinitionId, + value, + }); + }, + }), + + ...defineHandler({ + path: '/api/organizations/:organizationId/documents/:documentId/custom-properties/:propertyDefinitionId', + method: 'DELETE', + handler: async ({ params: { organizationId, documentId, propertyDefinitionId } }) => { + const docKey = `${organizationId}:${documentId}`; + const document = await documentStorage.getItem(docKey); + + assert(document, { status: 404 }); + + const valueKey = `${documentId}:${propertyDefinitionId}`; + + await documentCustomPropertyValueStorage.removeItem(valueKey); + }, + }), }; export const router = createRouter({ routes: inMemoryApiMock, strictTrailingSlash: false }); diff --git a/apps/papra-client/src/modules/demo/demo.storage.ts b/apps/papra-client/src/modules/demo/demo.storage.ts index 91a2edab..92fb87fa 100644 --- a/apps/papra-client/src/modules/demo/demo.storage.ts +++ b/apps/papra-client/src/modules/demo/demo.storage.ts @@ -1,4 +1,5 @@ import type { ApiKey } from '../api-keys/api-keys.types'; +import type { CustomPropertyDefinition } from '../custom-properties/custom-properties.types'; import type { Document } from '../documents/documents.types'; import type { Organization } from '../organizations/organizations.types'; import type { TaggingRule } from '../tagging-rules/tagging-rules.types'; @@ -9,6 +10,7 @@ import localStorageDriver from 'unstorage/drivers/localstorage'; import { trackingServices } from '../tracking/tracking.services'; import { DEMO_IS_SEEDED_KEY } from './demo.constants'; import { createId } from './demo.models'; +import { customPropertyDefinitionsFixtures } from './seed/custom-property-definitions.fixtures'; import { documentFixtures } from './seed/documents.fixtures'; import { tagsFixtures } from './seed/tags.fixtures'; @@ -20,6 +22,13 @@ export type DocumentFileStoredFile = { name: string; size: number; type: string; export type DocumentFileRemoteFile = { name: string; path: string }; export type DocumentFile = DocumentFileStoredFile | DocumentFileRemoteFile; +export type DocumentCustomPropertyValueStorage = { + id: string; + documentId: string; + propertyDefinitionId: string; + value: unknown; +}; + export const organizationStorage = prefixStorage(storage, 'organizations'); export const documentStorage = prefixStorage(storage, 'documents'); export const documentFileStorage = prefixStorage(storage, 'documentFiles'); @@ -28,6 +37,8 @@ export const tagDocumentStorage = prefixStorage<{ documentId: string; tagId: str export const taggingRuleStorage = prefixStorage(storage, 'taggingRules'); export const apiKeyStorage = prefixStorage(storage, 'apiKeys'); export const webhooksStorage = prefixStorage(storage, 'webhooks'); +export const customPropertyDefinitionStorage = prefixStorage(storage, 'customPropertyDefinitions'); +export const documentCustomPropertyValueStorage = prefixStorage(storage, 'documentCustomPropertyValues'); export async function clearDemoStorage() { await storage.clear(); @@ -90,6 +101,24 @@ export async function seedDemoStorage() { const tagsPromises = tagStorage.setItems(tags.map(tag => ({ key: tag.id, value: tag }))); + // Create custom property definitions + const customPropertyDefinitions = customPropertyDefinitionsFixtures.map((fixture, index) => ({ + id: createId({ prefix: 'cpd' }), + organizationId, + name: fixture.name, + key: fixture.key, + description: fixture.description ?? null, + type: fixture.type, + displayOrder: index, + options: fixture.options ?? [], + createdAt: lastMonth, + updatedAt: lastMonth, + })); + + const customPropertyDefinitionsPromises = customPropertyDefinitionStorage.setItems( + customPropertyDefinitions.map(def => ({ key: def.id, value: def })), + ); + const documentsPromises = documentFixtures.flatMap((fixture) => { const documentId = createId({ prefix: 'doc' }); @@ -126,11 +155,30 @@ export async function seedDemoStorage() { }; })); - return [documentPromise, documentFilePromise, tagDocumentPromise]; + const customPropertyValuePromises = (fixture.customProperties ?? []).map((prop) => { + const definition = customPropertyDefinitions.find(def => def.key === prop.key); + + if (!definition) { + return Promise.resolve(); + } + + const id = createId({ prefix: 'dcpv' }); + const valueKey = `${documentId}:${definition.id}`; + + return documentCustomPropertyValueStorage.setItem(valueKey, { + id, + documentId, + propertyDefinitionId: definition.id, + value: prop.value, + }); + }); + + return [documentPromise, documentFilePromise, tagDocumentPromise, ...customPropertyValuePromises]; }); await Promise.all([ tagsPromises, + customPropertyDefinitionsPromises, ...documentsPromises, ]); } diff --git a/apps/papra-client/src/modules/demo/search/demo.search.services.ts b/apps/papra-client/src/modules/demo/search/demo.search.services.ts index 2b579967..1510ac1d 100644 --- a/apps/papra-client/src/modules/demo/search/demo.search.services.ts +++ b/apps/papra-client/src/modules/demo/search/demo.search.services.ts @@ -5,6 +5,7 @@ import { parseSearchQuery } from '@papra/search-parser'; type DocumentCondition = (params: { document: Document }) => boolean; const falseCondition: DocumentCondition = () => false; +const trueCondition: DocumentCondition = () => true; export function someCorpusTokenStartsWith({ corpus, prefix }: { corpus: string | string []; prefix: string }): boolean { const lowerPrefix = prefix.toLowerCase(); @@ -149,6 +150,113 @@ function buildHasDateFilter({ expression }: { expression: FilterExpression }): D return ({ document }) => document.documentDate != null; } +function buildHasCustomPropertyFilter({ propertyKey }: { propertyKey: string }): DocumentCondition { + return ({ document }) => { + const prop = document.customProperties?.find(p => p.key === propertyKey); + + return prop !== undefined && prop.value != null; + }; +} + +function buildCustomPropertyFilterCondition({ field, expression }: { field: string; expression: FilterExpression }): DocumentCondition { + const { value, operator } = expression; + + return ({ document }) => { + const prop = document.customProperties?.find(p => p.key === field); + + if (!prop || prop.value == null) { + return false; + } + + switch (prop.type) { + case 'boolean': { + if (operator !== '=') { + return false; + } + + const boolValue = ['true', 'yes', '1', 'on', 'enabled'].includes(value.toLowerCase()); + + return prop.value === boolValue; + } + case 'number': { + const numValue = Number(value); + + if (Number.isNaN(numValue)) { + return false; + } + + const propNum = Number(prop.value); + + switch (operator) { + case '=': return propNum === numValue; + case '<': return propNum < numValue; + case '<=': return propNum <= numValue; + case '>': return propNum > numValue; + case '>=': return propNum >= numValue; + default: return false; + } + } + case 'date': { + const dateValue = getDateValue({ value }); + + if (Number.isNaN(dateValue.getTime())) { + return false; + } + + const propDate = new Date(prop.value as string); + + switch (operator) { + case '=': return propDate.toDateString() === dateValue.toDateString(); + case '<': return propDate < dateValue; + case '<=': return propDate <= dateValue; + case '>': return propDate > dateValue; + case '>=': return propDate >= dateValue; + default: return false; + } + } + case 'text': { + if (operator !== '=') { + return false; + } + + return someCorpusTokenStartsWith({ corpus: String(prop.value), prefix: value }); + } + case 'select': { + if (operator !== '=') { + return false; + } + + const propValue = prop.value as { key: string; name: string } | string; + const propKey = typeof propValue === 'object' ? propValue.key : propValue; + + return propKey.toLowerCase() === value.toLowerCase(); + } + case 'multi_select': { + if (operator !== '=') { + return false; + } + + const propValues = prop.value as Array<{ key: string; name: string } | string>; + + return propValues.some((v) => { + const optKey = typeof v === 'object' ? v.key : v; + + return optKey.toLowerCase() === value.toLowerCase(); + }); + } + default: { + if (operator !== '=') { + return false; + } + + return String(prop.value).toLowerCase() === value.toLowerCase(); + } + } + }; +} + +const KNOWN_FILTER_FIELDS = new Set(['tag', 'name', 'content', 'created', 'date', 'has']); + function buildExpressionCondition({ expression }: { expression: Expression }): DocumentCondition { switch (expression.type) { case 'text': return buildTextCondition({ expression }); @@ -166,11 +274,19 @@ function buildExpressionCondition({ expression }: { expression: Expression }): D switch (expression.value) { case 'tags': return buildHasTagsFilter({ expression }); case 'date': return buildHasDateFilter({ expression }); - default: return falseCondition; + default: + // has: — check if document has a non-null value for this property + return buildHasCustomPropertyFilter({ propertyKey: expression.value }); } - default: return falseCondition; + default: + // Unknown field — treat as a custom property key filter + if (!KNOWN_FILTER_FIELDS.has(expression.field)) { + return buildCustomPropertyFilterCondition({ field: expression.field, expression }); + } + + return falseCondition; } - case 'empty': return falseCondition; + case 'empty': return trueCondition; default: return falseCondition; } } diff --git a/apps/papra-client/src/modules/demo/seed/custom-property-definitions.fixtures.ts b/apps/papra-client/src/modules/demo/seed/custom-property-definitions.fixtures.ts new file mode 100644 index 00000000..278ddcb5 --- /dev/null +++ b/apps/papra-client/src/modules/demo/seed/custom-property-definitions.fixtures.ts @@ -0,0 +1,53 @@ +import type { CustomPropertySelectOption, CustomPropertyType } from '../../custom-properties/custom-properties.types'; + +export type DemoCustomPropertyDefinitionFixture = { + name: string; + key: string; + description?: string; + type: CustomPropertyType; + options?: CustomPropertySelectOption[]; +}; + +export const customPropertyDefinitionsFixtures: DemoCustomPropertyDefinitionFixture[] = [ + { + name: 'Status', + key: 'status', + description: 'Current processing status of the document', + type: 'select', + options: [ + { id: 'opt_status_pending', key: 'pending', name: 'Pending', displayOrder: 0 }, + { id: 'opt_status_reviewed', key: 'reviewed', name: 'Reviewed', displayOrder: 1 }, + { id: 'opt_status_archived', key: 'archived', name: 'Archived', displayOrder: 2 }, + ], + }, + { + name: 'Priority', + key: 'priority', + description: 'Investigation priority level', + type: 'select', + options: [ + { id: 'opt_priority_low', key: 'low', name: 'Low', displayOrder: 0 }, + { id: 'opt_priority_medium', key: 'medium', name: 'Medium', displayOrder: 1 }, + { id: 'opt_priority_high', key: 'high', name: 'High', displayOrder: 2 }, + { id: 'opt_priority_critical', key: 'critical', name: 'Critical', displayOrder: 3 }, + ], + }, + { + name: 'Amount', + key: 'amount', + description: 'Monetary amount in GBP', + type: 'number', + }, + { + name: 'Confidential', + key: 'confidential', + description: 'Whether this document is confidential', + type: 'boolean', + }, + { + name: 'Reference', + key: 'reference', + description: 'External reference number or identifier', + type: 'text', + }, +] as const satisfies DemoCustomPropertyDefinitionFixture[]; diff --git a/apps/papra-client/src/modules/demo/seed/documents/001.demo-document.ts b/apps/papra-client/src/modules/demo/seed/documents/001.demo-document.ts index a4ab8203..7c5db8a0 100644 --- a/apps/papra-client/src/modules/demo/seed/documents/001.demo-document.ts +++ b/apps/papra-client/src/modules/demo/seed/documents/001.demo-document.ts @@ -8,6 +8,12 @@ const demoDocumentFixture: DemoDocumentFixture = { mimeType: 'application/pdf', size: 31564, tags: ['Cases'], + customProperties: [ + { key: 'status', value: 'reviewed' }, + { key: 'priority', value: 'critical' }, + { key: 'confidential', value: true }, + { key: 'reference', value: 'CASE-2026-001' }, + ], content: ` blackmail letter from reginald thornton fiennes date january 20 2026 reggie com to sherlock holmes sleuth subject the secrets you diff --git a/apps/papra-client/src/modules/demo/seed/documents/002.demo-document.ts b/apps/papra-client/src/modules/demo/seed/documents/002.demo-document.ts index 3b51af84..82aa474b 100644 --- a/apps/papra-client/src/modules/demo/seed/documents/002.demo-document.ts +++ b/apps/papra-client/src/modules/demo/seed/documents/002.demo-document.ts @@ -8,6 +8,13 @@ const demoDocumentFixture: DemoDocumentFixture = { mimeType: 'application/pdf', size: 54921, tags: ['Legal', 'Cases'], + customProperties: [ + { key: 'status', value: 'reviewed' }, + { key: 'priority', value: 'high' }, + { key: 'amount', value: 5000 }, + { key: 'confidential', value: true }, + { key: 'reference', value: 'SH-2024-087' }, + ], content: ` contract private investigation pemberton number sh 2024 087 date 22nd september parties service provider sherlock holmes consulting detective 221b baker diff --git a/apps/papra-client/src/modules/demo/seed/documents/006.demo-document.ts b/apps/papra-client/src/modules/demo/seed/documents/006.demo-document.ts index 588fe704..edeaf7ed 100644 --- a/apps/papra-client/src/modules/demo/seed/documents/006.demo-document.ts +++ b/apps/papra-client/src/modules/demo/seed/documents/006.demo-document.ts @@ -8,6 +8,10 @@ const demoDocumentFixture: DemoDocumentFixture = { mimeType: 'application/pdf', size: 50374, tags: ['Receipts'], + customProperties: [ + { key: 'status', value: 'archived' }, + { key: 'amount', value: 19.56 }, + ], content: ` receipt for groceries date january 20 2026 store tesco superstore 123 regent st london sw1e 7na order number tcgro000023 item diff --git a/apps/papra-client/src/modules/demo/seed/documents/007.demo-document.ts b/apps/papra-client/src/modules/demo/seed/documents/007.demo-document.ts index b9fb3202..5c7a1c88 100644 --- a/apps/papra-client/src/modules/demo/seed/documents/007.demo-document.ts +++ b/apps/papra-client/src/modules/demo/seed/documents/007.demo-document.ts @@ -8,6 +8,10 @@ const demoDocumentFixture: DemoDocumentFixture = { mimeType: 'application/pdf', size: 38421, tags: ['Receipts', 'Property'], + customProperties: [ + { key: 'status', value: 'archived' }, + { key: 'amount', value: 2000 }, + ], content: ` rent receipt april 2025 221b baker street marylebone london nw1 6xe date 1st received from mr sherlock holmes for first diff --git a/apps/papra-client/src/modules/demo/seed/documents/008.demo-document.ts b/apps/papra-client/src/modules/demo/seed/documents/008.demo-document.ts index bffbd7f0..52f6b951 100644 --- a/apps/papra-client/src/modules/demo/seed/documents/008.demo-document.ts +++ b/apps/papra-client/src/modules/demo/seed/documents/008.demo-document.ts @@ -8,6 +8,10 @@ const demoDocumentFixture: DemoDocumentFixture = { mimeType: 'application/pdf', size: 37805, tags: ['Receipts', 'Property'], + customProperties: [ + { key: 'status', value: 'archived' }, + { key: 'amount', value: 2000 }, + ], content: ` rent receipt july 2025 221b baker street marylebone london nw1 6xe date 1st received from mr sherlock holmes for first diff --git a/apps/papra-client/src/modules/demo/seed/documents/012.demo-document.ts b/apps/papra-client/src/modules/demo/seed/documents/012.demo-document.ts index 67fd458f..085dae8e 100644 --- a/apps/papra-client/src/modules/demo/seed/documents/012.demo-document.ts +++ b/apps/papra-client/src/modules/demo/seed/documents/012.demo-document.ts @@ -8,6 +8,11 @@ const demoDocumentFixture: DemoDocumentFixture = { mimeType: 'application/pdf', size: 51229, tags: ['Receipts'], + customProperties: [ + { key: 'status', value: 'archived' }, + { key: 'amount', value: 3260 }, + { key: 'reference', value: 'SL-2025-1108' }, + ], content: ` violin receipt vendor information transaction details item description price quantity customized the baker 2 500 00 1 hard case with diff --git a/apps/papra-client/src/modules/demo/seed/fixtures.types.ts b/apps/papra-client/src/modules/demo/seed/fixtures.types.ts index b34335e9..34b3c59c 100644 --- a/apps/papra-client/src/modules/demo/seed/fixtures.types.ts +++ b/apps/papra-client/src/modules/demo/seed/fixtures.types.ts @@ -1,5 +1,11 @@ +import type { DemoCustomPropertyDefinitionFixture } from './custom-property-definitions.fixtures'; import type { DemoTagFixtureNames } from './tags.fixtures'; +export type DemoDocumentCustomPropertyValue = { + key: DemoCustomPropertyDefinitionFixture['key']; + value: unknown; +}; + export type DemoDocumentFixture = { name: string; date: Date; @@ -8,4 +14,5 @@ export type DemoDocumentFixture = { tags: DemoTagFixtureNames[]; mimeType: string; size: number; + customProperties?: DemoDocumentCustomPropertyValue[]; };