From 25db74f81e2ec43f9fee207e25b6ad112e2a137e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:49:18 +0800 Subject: [PATCH 1/2] chore: prettier-format the repo (clear pre-existing format:check debt) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs 'pnpm format' (prettier --write) across the workspace. main had ~115 files that failed 'pnpm format:check', so the CI Format-check step was red for every PR regardless of its contents. Pure formatting — no logic changes; build and typecheck stay green. Co-Authored-By: Claude Opus 4.8 --- packages/all/scripts/compile-marketplace.mjs | 69 ++++-- packages/compliance/objectstack.manifest.json | 8 +- .../compliance/src/apps/compliance.app.ts | 32 ++- .../dashboards/control_posture.dashboard.ts | 15 +- packages/compliance/src/data/index.ts | 214 +++++++++++++++-- packages/compliance/src/flows/index.ts | 6 +- .../objects/compliance_assessment.object.ts | 8 +- .../src/objects/compliance_evidence.hook.ts | 6 +- .../src/objects/compliance_evidence.object.ts | 11 +- .../objects/compliance_framework.object.ts | 9 +- .../src/profiles/compliance_admin.profile.ts | 36 ++- .../src/profiles/control_owner.profile.ts | 36 ++- packages/compliance/src/sharing/index.ts | 6 +- packages/compliance/src/translations/en.ts | 123 ++++++++-- packages/compliance/src/translations/zh-CN.ts | 76 ++++-- .../src/views/compliance_assessment.view.ts | 11 +- .../src/views/compliance_control.view.ts | 15 +- .../src/views/compliance_evidence.view.ts | 14 +- packages/content/objectstack.manifest.json | 8 +- .../content/src/actions/publish_now.action.ts | 3 +- .../content/src/actions/suggest_cta.action.ts | 9 +- .../editorial_calendar.dashboard.ts | 4 +- .../dashboards/roi_by_channel.dashboard.ts | 12 +- .../dashboards/today_workbench.dashboard.ts | 7 +- packages/content/src/data/index.ts | 129 +++++++++-- .../src/objects/content_metric.object.ts | 6 +- .../src/objects/content_piece.object.ts | 11 +- .../src/objects/content_publication.object.ts | 6 +- .../src/objects/content_signal.object.ts | 2 +- packages/content/src/translations/en.ts | 131 ++++++++--- packages/content/src/translations/zh-CN.ts | 10 +- .../content/src/views/content_signal.view.ts | 7 +- packages/content/src/views/index.ts | 7 +- packages/contracts/objectstack.manifest.json | 9 +- .../src/actions/extract_terms.action.ts | 16 +- packages/contracts/src/flows/index.ts | 6 +- .../src/objects/contracts_contract.object.ts | 16 +- .../src/objects/contracts_party.object.ts | 5 +- packages/contracts/src/translations/en.ts | 60 ++++- packages/contracts/src/translations/zh-CN.ts | 8 +- .../src/views/contracts_contract.view.ts | 7 +- packages/expense/objectstack.manifest.json | 7 +- packages/expense/src/apps/expense.app.ts | 16 +- .../dashboards/expenses_overview.dashboard.ts | 3 +- packages/expense/src/data/index.ts | 186 +++++++++++++-- .../src/objects/expense_category.object.ts | 6 +- .../src/objects/expense_line.object.ts | 6 +- .../src/objects/expense_report.object.ts | 8 +- packages/expense/src/sharing/index.ts | 6 +- packages/expense/src/translations/en.ts | 58 ++++- packages/helpdesk/objectstack.manifest.json | 7 +- packages/helpdesk/src/apps/helpdesk.app.ts | 40 +++- .../dashboards/manager_overview.dashboard.ts | 10 +- packages/helpdesk/src/data/index.ts | 219 +++++++++++++----- .../src/objects/helpdesk_kb_article.object.ts | 7 +- .../src/objects/helpdesk_sla_policy.object.ts | 30 ++- .../src/objects/helpdesk_team.object.ts | 3 +- .../src/objects/helpdesk_ticket.object.ts | 13 +- .../helpdesk/src/portals/_portal-spec-shim.ts | 14 +- .../helpdesk/src/profiles/agent.profile.ts | 54 ++++- .../src/profiles/customer_portal.profile.ts | 54 ++++- .../src/profiles/helpdesk_admin.profile.ts | 54 ++++- packages/helpdesk/src/sharing/index.ts | 12 +- packages/helpdesk/src/translations/en.ts | 127 +++++++--- packages/helpdesk/src/translations/zh-CN.ts | 110 ++++++--- .../src/views/helpdesk_sla_policy.view.ts | 6 +- .../src/views/helpdesk_ticket.view.ts | 48 +++- packages/hr/objectstack.manifest.json | 7 +- packages/hr/src/apps/hr.app.ts | 32 ++- packages/hr/src/data/index.ts | 7 +- .../hr/src/objects/hr_department.object.ts | 5 +- packages/hr/src/objects/hr_document.object.ts | 6 +- .../src/objects/hr_time_off_request.object.ts | 17 +- packages/hr/src/translations/en.ts | 47 +++- packages/hr/src/translations/zh-CN.ts | 5 +- packages/hr/src/views/hr_document.view.ts | 7 +- packages/hr/src/views/hr_employee.view.ts | 7 +- .../hr/src/views/hr_time_off_request.view.ts | 19 +- .../procurement/objectstack.manifest.json | 8 +- .../procurement/src/apps/procurement.app.ts | 32 ++- packages/procurement/src/data/index.ts | 4 +- packages/procurement/src/flows/index.ts | 6 +- .../src/objects/procurement_order.object.ts | 9 +- .../src/objects/procurement_receipt.object.ts | 6 +- .../src/objects/procurement_request.object.ts | 8 +- .../src/objects/procurement_vendor.object.ts | 9 +- .../procurement/src/profiles/buyer.profile.ts | 36 ++- .../src/profiles/procurement_admin.profile.ts | 36 ++- packages/procurement/src/translations/en.ts | 60 ++++- .../procurement/src/translations/zh-CN.ts | 54 ++++- .../src/views/procurement_order.view.ts | 11 +- .../src/views/procurement_request.view.ts | 4 +- packages/project/objectstack.manifest.json | 7 +- packages/project/src/data/index.ts | 8 +- packages/project/src/data/projects.data.ts | 6 +- .../project/src/objects/pm_issue.object.ts | 10 +- .../src/objects/pm_milestone.object.ts | 6 +- .../project/src/objects/pm_project.object.ts | 9 +- .../project/src/objects/pm_resource.object.ts | 10 +- .../project/src/objects/pm_risk.object.ts | 15 +- .../src/objects/pm_timesheet.object.ts | 10 +- packages/project/src/translations/en.ts | 66 ++++-- packages/project/src/translations/zh-CN.ts | 37 ++- packages/project/src/views/pm_issue.view.ts | 15 +- .../project/src/views/pm_milestone.view.ts | 25 +- packages/project/src/views/pm_project.view.ts | 5 +- packages/project/src/views/pm_risk.view.ts | 14 +- packages/todo/objectstack.manifest.json | 8 +- packages/todo/src/objects/todo_task.object.ts | 13 +- packages/todo/src/translations/en.ts | 15 +- packages/todo/src/translations/es-ES.ts | 37 ++- packages/todo/src/translations/index.ts | 1 - packages/todo/src/translations/ja-JP.ts | 20 +- scripts/run-qa.mjs | 9 +- 114 files changed, 2377 insertions(+), 689 deletions(-) diff --git a/packages/all/scripts/compile-marketplace.mjs b/packages/all/scripts/compile-marketplace.mjs index 2b7da54..89d0066 100644 --- a/packages/all/scripts/compile-marketplace.mjs +++ b/packages/all/scripts/compile-marketplace.mjs @@ -36,13 +36,7 @@ // and the CLI's serve path validates with `ObjectStackDefinitionSchema` (schema // only), so the single-app / namespace-prefix gates correctly do not apply. -import { - readFileSync, - writeFileSync, - mkdirSync, - readdirSync, - existsSync, -} from 'node:fs'; +import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -70,11 +64,32 @@ const safeFilename = (manifestId) => `${manifestId.replace(/[^a-zA-Z0-9._-]/g, ' // Arrays concatenated at the environment layer (mirrors `composeStacks`' // CONCAT_ARRAY_FIELDS, minus the singletons we de-dupe below). const CONCAT_FIELDS = [ - 'translations', 'objectExtensions', 'objects', 'apps', 'views', 'pages', - 'dashboards', 'reports', 'actions', 'themes', 'flows', 'jobs', - 'emailTemplates', 'sharingRules', 'policies', 'apis', 'webhooks', 'agents', - 'skills', 'hooks', 'mappings', 'analyticsCubes', 'connectors', 'datasources', - 'portals', 'data', + 'translations', + 'objectExtensions', + 'objects', + 'apps', + 'views', + 'pages', + 'dashboards', + 'reports', + 'actions', + 'themes', + 'flows', + 'jobs', + 'emailTemplates', + 'sharingRules', + 'policies', + 'apis', + 'webhooks', + 'agents', + 'skills', + 'hooks', + 'mappings', + 'analyticsCubes', + 'connectors', + 'datasources', + 'portals', + 'data', ]; // Environment-level singletons keyed by `name`. Two installed apps can each ship @@ -126,7 +141,9 @@ function installFromWorkspace() { function readStore(dir) { if (!existsSync(dir)) return []; const out = []; - for (const file of readdirSync(dir).filter((f) => f.endsWith('.json')).sort()) { + for (const file of readdirSync(dir) + .filter((f) => f.endsWith('.json')) + .sort()) { let entry; try { entry = JSON.parse(readFileSync(join(dir, file), 'utf8')); @@ -137,7 +154,8 @@ function readStore(dir) { // Accept either the runtime wrapper ({ manifest: }) or a bare // artifact dropped straight into the folder. const artifact = entry?.manifest?.objects || entry?.manifest?.apps ? entry.manifest : entry; - const source = artifact?.manifest?.namespace || entry?.manifestId || file.replace(/\.json$/, ''); + const source = + artifact?.manifest?.namespace || entry?.manifestId || file.replace(/\.json$/, ''); out.push({ source, artifact }); } return out; @@ -148,8 +166,12 @@ function readInstalledArtifacts() { const cache = readStore(INSTALLED_DIR); const live = readStore(LIVE_INSTALL_DIR); if (live.length > 0) { - log(` ⓘ folding in ${live.length} live marketplace install(s) from .objectstack/installed-packages/`); - log(' (the runtime rehydrates that folder too — run `start` from a clean cwd to avoid double-load)'); + log( + ` ⓘ folding in ${live.length} live marketplace install(s) from .objectstack/installed-packages/`, + ); + log( + ' (the runtime rehydrates that folder too — run `start` from a clean cwd to avoid double-load)', + ); } // De-dupe by namespace; cache wins (it's the deterministic workspace copy). const seen = new Set(cache.map((e) => e.source)); @@ -194,7 +216,10 @@ function compile(store) { for (const field of DEDUP_BY_NAME) { for (const item of artifact[field] ?? []) { if (!dedup[field].has(item.name)) dedup[field].set(item.name, item); - else log(` ! ${field.slice(0, -1)} '${item.name}' from '${source}' shadowed (already installed)`); + else + log( + ` ! ${field.slice(0, -1)} '${item.name}' from '${source}' shadowed (already installed)`, + ); } } @@ -213,7 +238,11 @@ function compile(store) { if (requires.size > 0) env.requires = [...requires]; supportedLocales.add(defaultLocale); - env.i18n = { defaultLocale, supportedLocales: [...supportedLocales], fallbackLocale: defaultLocale }; + env.i18n = { + defaultLocale, + supportedLocales: [...supportedLocales], + fallbackLocale: defaultLocale, + }; return env; } @@ -237,7 +266,9 @@ mkdirSync(dirname(OUT), { recursive: true }); writeFileSync(OUT, `${JSON.stringify(env, null, 2)}\n`); log(''); -log(`✓ Composed ${store.length} apps · ${env.objects?.length ?? 0} objects · ${env.flows?.length ?? 0} flows`); +log( + `✓ Composed ${store.length} apps · ${env.objects?.length ?? 0} objects · ${env.flows?.length ?? 0} flows`, +); log(` apps: ${(env.apps ?? []).map((a) => a.name).join(', ')}`); log(` installed-packages: ${INSTALLED_DIR.replace(`${PACKAGES_DIR}/`, '')}`); log(` → ${OUT.replace(`${PACKAGES_DIR}/`, '')}`); diff --git a/packages/compliance/objectstack.manifest.json b/packages/compliance/objectstack.manifest.json index ac2e26d..dd9a33c 100644 --- a/packages/compliance/objectstack.manifest.json +++ b/packages/compliance/objectstack.manifest.json @@ -13,7 +13,12 @@ "iconUrl": "https://cdn.objectos.app/icons/compliance.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/compliance", "tags": ["compliance", "audit", "evidence", "soc2", "iso27001"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md", "translations": { "zh-CN": { @@ -23,4 +28,3 @@ } } } - diff --git a/packages/compliance/src/apps/compliance.app.ts b/packages/compliance/src/apps/compliance.app.ts index f525c48..de37ef2 100644 --- a/packages/compliance/src/apps/compliance.app.ts +++ b/packages/compliance/src/apps/compliance.app.ts @@ -17,9 +17,33 @@ export const ComplianceApp = App.create({ label: 'Control Posture', icon: 'gauge', }, - { id: 'nav_framework', type: 'object', objectName: 'compliance_framework', label: 'Frameworks', icon: 'shield-check' }, - { id: 'nav_control', type: 'object', objectName: 'compliance_control', label: 'Controls', icon: 'shield' }, - { id: 'nav_evidence', type: 'object', objectName: 'compliance_evidence', label: 'Evidence', icon: 'paperclip' }, - { id: 'nav_assessment', type: 'object', objectName: 'compliance_assessment', label: 'Assessments', icon: 'clipboard-check' }, + { + id: 'nav_framework', + type: 'object', + objectName: 'compliance_framework', + label: 'Frameworks', + icon: 'shield-check', + }, + { + id: 'nav_control', + type: 'object', + objectName: 'compliance_control', + label: 'Controls', + icon: 'shield', + }, + { + id: 'nav_evidence', + type: 'object', + objectName: 'compliance_evidence', + label: 'Evidence', + icon: 'paperclip', + }, + { + id: 'nav_assessment', + type: 'object', + objectName: 'compliance_assessment', + label: 'Assessments', + icon: 'clipboard-check', + }, ], }); diff --git a/packages/compliance/src/dashboards/control_posture.dashboard.ts b/packages/compliance/src/dashboards/control_posture.dashboard.ts index e283af3..689a3f6 100644 --- a/packages/compliance/src/dashboards/control_posture.dashboard.ts +++ b/packages/compliance/src/dashboards/control_posture.dashboard.ts @@ -19,8 +19,7 @@ import type { Dashboard } from '@objectstack/spec/ui'; export const ControlPostureDashboard: Dashboard = { name: 'control_posture_dashboard', label: 'Control Posture', - description: - 'Pass rate, failing controls, expiring evidence, and in-flight assessments.', + description: 'Pass rate, failing controls, expiring evidence, and in-flight assessments.', columns: 12, gap: 4, @@ -30,7 +29,12 @@ export const ControlPostureDashboard: Dashboard = { showTitle: true, showDescription: true, actions: [ - { label: 'New Assessment', icon: 'Plus', actionType: 'modal', actionUrl: 'create_assessment' }, + { + label: 'New Assessment', + icon: 'Plus', + actionType: 'modal', + actionUrl: 'create_assessment', + }, ], }, @@ -93,7 +97,10 @@ export const ControlPostureDashboard: Dashboard = { options: { columns: ['code', 'title', 'framework', 'criticality', 'last_assessed_at'], pageSize: 10, - sort: [{ field: 'criticality', order: 'asc' }, { field: 'code', order: 'asc' }], + sort: [ + { field: 'criticality', order: 'asc' }, + { field: 'code', order: 'asc' }, + ], }, }, { diff --git a/packages/compliance/src/data/index.ts b/packages/compliance/src/data/index.ts index dcbebc6..ba411d1 100644 --- a/packages/compliance/src/data/index.ts +++ b/packages/compliance/src/data/index.ts @@ -19,9 +19,31 @@ const frameworks = defineDataset(Framework, { mode: 'upsert', externalId: 'short_name', records: [ - { short_name: 'SOC2', full_name: 'SOC 2 Type II — Trust Services Criteria (2017)', family: 'soc2', version: '2017', status: 'active', next_audit_date: cel`daysFromNow(120)`, auditor: 'Acme CPA LLP' }, - { short_name: 'ISO27001', full_name: 'ISO/IEC 27001:2022 Information Security Management', family: 'iso27001', version: '2022', status: 'adopted', next_audit_date: cel`daysFromNow(300)`, auditor: 'Bureau Veritas' }, - { short_name: 'GDPR', full_name: 'General Data Protection Regulation (EU) 2016/679', family: 'gdpr', version: '2016', status: 'active' }, + { + short_name: 'SOC2', + full_name: 'SOC 2 Type II — Trust Services Criteria (2017)', + family: 'soc2', + version: '2017', + status: 'active', + next_audit_date: cel`daysFromNow(120)`, + auditor: 'Acme CPA LLP', + }, + { + short_name: 'ISO27001', + full_name: 'ISO/IEC 27001:2022 Information Security Management', + family: 'iso27001', + version: '2022', + status: 'adopted', + next_audit_date: cel`daysFromNow(300)`, + auditor: 'Bureau Veritas', + }, + { + short_name: 'GDPR', + full_name: 'General Data Protection Regulation (EU) 2016/679', + family: 'gdpr', + version: '2016', + status: 'active', + }, ], }); @@ -29,12 +51,72 @@ const controls = defineDataset(Control, { mode: 'upsert', externalId: 'code', records: [ - { code: 'CC6.1', title: 'Logical Access Controls', framework: 'SOC2', category: 'access', criticality: 'high', last_status: 'passed', review_frequency_days: 90, last_assessed_at: cel`daysAgo(20)`, description: 'The entity implements logical access controls to protect data.' }, - { code: 'CC7.1', title: 'Vulnerability Management', framework: 'SOC2', category: 'change', criticality: 'high', last_status: 'failed', review_frequency_days: 90, last_assessed_at: cel`daysAgo(5)`, description: 'Vulnerability scans run monthly; remediation in SLA.' }, - { code: 'CC8.1', title: 'Change Management', framework: 'SOC2', category: 'change', criticality: 'medium', last_status: 'partial', review_frequency_days: 90, last_assessed_at: cel`daysAgo(50)`, description: 'All production changes are peer-reviewed and approved.' }, - { code: 'A.5.1', title: 'Information Security Policies', framework: 'ISO27001', category: 'risk', criticality: 'high', last_status: 'passed', review_frequency_days: 180, last_assessed_at: cel`daysAgo(100)`, description: 'Information security policies approved by management & published.' }, - { code: 'A.8.16', title: 'Monitoring Activities', framework: 'ISO27001', category: 'incident', criticality: 'medium', last_status: 'not_tested', review_frequency_days: 90, description: 'Networks, systems and apps monitored for anomalous behaviour.' }, - { code: 'Art.32', title: 'Security of Processing', framework: 'GDPR', category: 'data', criticality: 'high', last_status: 'passed', review_frequency_days: 180, last_assessed_at: cel`daysAgo(200)`, description: 'Appropriate technical & organisational measures to ensure security of personal data. Past review-frequency window.' }, + { + code: 'CC6.1', + title: 'Logical Access Controls', + framework: 'SOC2', + category: 'access', + criticality: 'high', + last_status: 'passed', + review_frequency_days: 90, + last_assessed_at: cel`daysAgo(20)`, + description: 'The entity implements logical access controls to protect data.', + }, + { + code: 'CC7.1', + title: 'Vulnerability Management', + framework: 'SOC2', + category: 'change', + criticality: 'high', + last_status: 'failed', + review_frequency_days: 90, + last_assessed_at: cel`daysAgo(5)`, + description: 'Vulnerability scans run monthly; remediation in SLA.', + }, + { + code: 'CC8.1', + title: 'Change Management', + framework: 'SOC2', + category: 'change', + criticality: 'medium', + last_status: 'partial', + review_frequency_days: 90, + last_assessed_at: cel`daysAgo(50)`, + description: 'All production changes are peer-reviewed and approved.', + }, + { + code: 'A.5.1', + title: 'Information Security Policies', + framework: 'ISO27001', + category: 'risk', + criticality: 'high', + last_status: 'passed', + review_frequency_days: 180, + last_assessed_at: cel`daysAgo(100)`, + description: 'Information security policies approved by management & published.', + }, + { + code: 'A.8.16', + title: 'Monitoring Activities', + framework: 'ISO27001', + category: 'incident', + criticality: 'medium', + last_status: 'not_tested', + review_frequency_days: 90, + description: 'Networks, systems and apps monitored for anomalous behaviour.', + }, + { + code: 'Art.32', + title: 'Security of Processing', + framework: 'GDPR', + category: 'data', + criticality: 'high', + last_status: 'passed', + review_frequency_days: 180, + last_assessed_at: cel`daysAgo(200)`, + description: + 'Appropriate technical & organisational measures to ensure security of personal data. Past review-frequency window.', + }, ], }); @@ -42,13 +124,65 @@ const evidence = defineDataset(Evidence, { mode: 'upsert', externalId: 'title', records: [ - { title: 'Q1 access review export', control: 'CC6.1', evidence_type: 'config', status: 'approved', collected_on: cel`daysAgo(20)`, expires_on: cel`daysFromNow(70)`, notes: 'Quarterly access review CSV.' }, - { title: 'March 2026 Nessus scan', control: 'CC7.1', evidence_type: 'pentest', status: 'approved', collected_on: cel`daysAgo(35)`, expires_on: cel`daysFromNow(25)`, notes: 'Monthly scan — next due in 25 days; will trigger expiring flow at T-7.' }, - { title: 'PR review screenshots — Mar', control: 'CC8.1', evidence_type: 'screenshot', status: 'submitted', collected_on: cel`daysAgo(2)`, expires_on: cel`daysFromNow(88)` }, - { title: 'InfoSec Policy v3.2', control: 'A.5.1', evidence_type: 'policy', status: 'approved', collected_on: cel`daysAgo(100)`, expires_on: cel`daysFromNow(265)` }, - { title: 'Datadog log retention setting', control: 'A.8.16', evidence_type: 'screenshot', status: 'pending', collected_on: cel`daysAgo(1)` }, - { title: 'DPA — Cloudwell hosting', control: 'Art.32', evidence_type: 'audit_letter', status: 'approved', collected_on: cel`daysAgo(200)`, expires_on: cel`daysAgo(10)`, notes: 'Already expired — should trigger auto-expire flow.' }, - { title: 'Encryption at rest config — RDS', control: 'Art.32', evidence_type: 'config', status: 'approved', collected_on: cel`daysAgo(40)`, expires_on: cel`daysFromNow(7)`, notes: 'Will fire 7-day expiring alert today.' }, + { + title: 'Q1 access review export', + control: 'CC6.1', + evidence_type: 'config', + status: 'approved', + collected_on: cel`daysAgo(20)`, + expires_on: cel`daysFromNow(70)`, + notes: 'Quarterly access review CSV.', + }, + { + title: 'March 2026 Nessus scan', + control: 'CC7.1', + evidence_type: 'pentest', + status: 'approved', + collected_on: cel`daysAgo(35)`, + expires_on: cel`daysFromNow(25)`, + notes: 'Monthly scan — next due in 25 days; will trigger expiring flow at T-7.', + }, + { + title: 'PR review screenshots — Mar', + control: 'CC8.1', + evidence_type: 'screenshot', + status: 'submitted', + collected_on: cel`daysAgo(2)`, + expires_on: cel`daysFromNow(88)`, + }, + { + title: 'InfoSec Policy v3.2', + control: 'A.5.1', + evidence_type: 'policy', + status: 'approved', + collected_on: cel`daysAgo(100)`, + expires_on: cel`daysFromNow(265)`, + }, + { + title: 'Datadog log retention setting', + control: 'A.8.16', + evidence_type: 'screenshot', + status: 'pending', + collected_on: cel`daysAgo(1)`, + }, + { + title: 'DPA — Cloudwell hosting', + control: 'Art.32', + evidence_type: 'audit_letter', + status: 'approved', + collected_on: cel`daysAgo(200)`, + expires_on: cel`daysAgo(10)`, + notes: 'Already expired — should trigger auto-expire flow.', + }, + { + title: 'Encryption at rest config — RDS', + control: 'Art.32', + evidence_type: 'config', + status: 'approved', + collected_on: cel`daysAgo(40)`, + expires_on: cel`daysFromNow(7)`, + notes: 'Will fire 7-day expiring alert today.', + }, ], }); @@ -56,19 +190,49 @@ const assessments = defineDataset(Assessment, { mode: 'upsert', externalId: 'title', records: [ - { title: '2026-Q1 access review', control: 'CC6.1', cycle: '2026-Q1', assessed_at: cel`daysAgo(20)`, status: 'passed', finding: 'All terminated users disabled within 24h. No exceptions found.' }, - { title: '2026-Q1 vuln scan review', control: 'CC7.1', cycle: '2026-Q1', assessed_at: cel`daysAgo(5)`, status: 'failed', + { + title: '2026-Q1 access review', + control: 'CC6.1', + cycle: '2026-Q1', + assessed_at: cel`daysAgo(20)`, + status: 'passed', + finding: 'All terminated users disabled within 24h. No exceptions found.', + }, + { + title: '2026-Q1 vuln scan review', + control: 'CC7.1', + cycle: '2026-Q1', + assessed_at: cel`daysAgo(5)`, + status: 'failed', finding: 'Two high-severity CVEs open past 30-day SLA on edge servers.', remediation_plan: 'Patch edge servers in next maintenance window (2026-04-15).', - remediation_due: cel`daysFromNow(14)` }, - { title: '2026-Q1 change-mgmt sample', control: 'CC8.1', cycle: '2026-Q1', assessed_at: cel`daysAgo(50)`, status: 'partial', + remediation_due: cel`daysFromNow(14)`, + }, + { + title: '2026-Q1 change-mgmt sample', + control: 'CC8.1', + cycle: '2026-Q1', + assessed_at: cel`daysAgo(50)`, + status: 'partial', finding: '3 of 25 PRs sampled lacked documented reviewer.', remediation_plan: 'Enforce required-reviewer via repo settings; re-test in Q2.', - remediation_due: cel`daysFromNow(30)` }, - { title: '2025-H2 policy attestation', control: 'A.5.1', cycle: '2025-H2', assessed_at: cel`daysAgo(100)`, status: 'passed', - finding: 'All 47 employees attested.' }, - { title: '2026-Q1 GDPR Art.32 review', control: 'Art.32', cycle: '2026-Q1', status: 'in_progress', - finding: 'Encryption-at-rest confirmed; key rotation policy under review.' }, + remediation_due: cel`daysFromNow(30)`, + }, + { + title: '2025-H2 policy attestation', + control: 'A.5.1', + cycle: '2025-H2', + assessed_at: cel`daysAgo(100)`, + status: 'passed', + finding: 'All 47 employees attested.', + }, + { + title: '2026-Q1 GDPR Art.32 review', + control: 'Art.32', + cycle: '2026-Q1', + status: 'in_progress', + finding: 'Encryption-at-rest confirmed; key rotation policy under review.', + }, ], }); diff --git a/packages/compliance/src/flows/index.ts b/packages/compliance/src/flows/index.ts index d918ef7..f52dad3 100644 --- a/packages/compliance/src/flows/index.ts +++ b/packages/compliance/src/flows/index.ts @@ -4,8 +4,4 @@ import { EvidenceExpiringFlow } from './evidence_expiring.flow'; import { EvidenceAutoExpireFlow } from './evidence_auto_expire.flow'; import { FailedControlEscalationFlow } from './failed_control_escalation.flow'; -export const allFlows = [ - EvidenceExpiringFlow, - EvidenceAutoExpireFlow, - FailedControlEscalationFlow, -]; +export const allFlows = [EvidenceExpiringFlow, EvidenceAutoExpireFlow, FailedControlEscalationFlow]; diff --git a/packages/compliance/src/objects/compliance_assessment.object.ts b/packages/compliance/src/objects/compliance_assessment.object.ts index 832ff7c..71a7118 100644 --- a/packages/compliance/src/objects/compliance_assessment.object.ts +++ b/packages/compliance/src/objects/compliance_assessment.object.ts @@ -81,7 +81,13 @@ export const Assessment = ObjectSchema.create({ type: 'state_machine', name: 'assessment_lifecycle', field: 'status', - transitions: {planned:["in_progress"], in_progress:["passed", "failed", "partial"], passed:["in_progress"], failed:["in_progress"], partial:["in_progress"]}, + transitions: { + planned: ['in_progress'], + in_progress: ['passed', 'failed', 'partial'], + passed: ['in_progress'], + failed: ['in_progress'], + partial: ['in_progress'], + }, message: 'Illegal status transition.', }, { diff --git a/packages/compliance/src/objects/compliance_evidence.hook.ts b/packages/compliance/src/objects/compliance_evidence.hook.ts index a596bcd..545f05a 100644 --- a/packages/compliance/src/objects/compliance_evidence.hook.ts +++ b/packages/compliance/src/objects/compliance_evidence.hook.ts @@ -33,11 +33,7 @@ const evidenceHook: Hook = { if (user?.id) input.approved_by = user.id; } - if ( - event === 'beforeUpdate' && - previous && - input.status === 'rejected' - ) { + if (event === 'beforeUpdate' && previous && input.status === 'rejected') { input.approved_by = null; } }, diff --git a/packages/compliance/src/objects/compliance_evidence.object.ts b/packages/compliance/src/objects/compliance_evidence.object.ts index 7a27082..588ec71 100644 --- a/packages/compliance/src/objects/compliance_evidence.object.ts +++ b/packages/compliance/src/objects/compliance_evidence.object.ts @@ -12,8 +12,7 @@ export const Evidence = ObjectSchema.create({ label: 'Evidence', pluralLabel: 'Evidence', icon: 'paperclip', - description: - 'Proof (screenshot, exported log, policy doc) that supports one or more controls.', + description: 'Proof (screenshot, exported log, policy doc) that supports one or more controls.', fieldGroups: [ { key: 'core', label: 'Evidence', icon: 'paperclip' }, @@ -119,7 +118,13 @@ export const Evidence = ObjectSchema.create({ type: 'state_machine', name: 'evidence_lifecycle', field: 'status', - transitions: {pending:["submitted"], submitted:["approved", "rejected"], approved:["expired"], rejected:["pending"], expired:["pending"]}, + transitions: { + pending: ['submitted'], + submitted: ['approved', 'rejected'], + approved: ['expired'], + rejected: ['pending'], + expired: ['pending'], + }, message: 'Illegal status transition.', }, { diff --git a/packages/compliance/src/objects/compliance_framework.object.ts b/packages/compliance/src/objects/compliance_framework.object.ts index aabd134..c4b0686 100644 --- a/packages/compliance/src/objects/compliance_framework.object.ts +++ b/packages/compliance/src/objects/compliance_framework.object.ts @@ -12,8 +12,7 @@ export const Framework = ObjectSchema.create({ label: 'Framework', pluralLabel: 'Frameworks', icon: 'shield-check', - description: - 'A compliance standard you are certifying against (SOC 2, ISO 27001, HIPAA, etc.).', + description: 'A compliance standard you are certifying against (SOC 2, ISO 27001, HIPAA, etc.).', fields: { short_name: Field.text({ @@ -73,11 +72,7 @@ export const Framework = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['short_name'] }, - { fields: ['family'] }, - { fields: ['status'] }, - ], + indexes: [{ fields: ['short_name'] }, { fields: ['family'] }, { fields: ['status'] }], titleFormat: tmpl`{{record.short_name}}`, displayNameField: 'short_name', diff --git a/packages/compliance/src/profiles/compliance_admin.profile.ts b/packages/compliance/src/profiles/compliance_admin.profile.ts index a905a1a..fcc585e 100644 --- a/packages/compliance/src/profiles/compliance_admin.profile.ts +++ b/packages/compliance/src/profiles/compliance_admin.profile.ts @@ -9,9 +9,37 @@ export const ComplianceAdminProfile = { label: 'Compliance Admin', isProfile: true, objects: { - compliance_framework: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - compliance_control: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - compliance_evidence: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - compliance_assessment: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, + compliance_framework: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + compliance_control: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + compliance_evidence: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + compliance_assessment: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, }, }; diff --git a/packages/compliance/src/profiles/control_owner.profile.ts b/packages/compliance/src/profiles/control_owner.profile.ts index d022740..b435662 100644 --- a/packages/compliance/src/profiles/control_owner.profile.ts +++ b/packages/compliance/src/profiles/control_owner.profile.ts @@ -9,9 +9,37 @@ export const ControlOwnerProfile = { label: 'Control Owner', isProfile: true, objects: { - compliance_framework: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - compliance_control: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - compliance_evidence: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - compliance_assessment: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, + compliance_framework: { + allowCreate: false, + allowRead: true, + allowEdit: false, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + compliance_control: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + compliance_evidence: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + compliance_assessment: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, }, }; diff --git a/packages/compliance/src/sharing/index.ts b/packages/compliance/src/sharing/index.ts index 7554e6c..507575a 100644 --- a/packages/compliance/src/sharing/index.ts +++ b/packages/compliance/src/sharing/index.ts @@ -3,6 +3,10 @@ export const RoleHierarchy = { roles: [ { name: 'compliance_admin', label: 'Compliance Admin', parentRole: null as string | null }, - { name: 'compliance_control_owner', label: 'Control Owner', parentRole: 'compliance_admin' as string | null }, + { + name: 'compliance_control_owner', + label: 'Control Owner', + parentRole: 'compliance_admin' as string | null, + }, ], }; diff --git a/packages/compliance/src/translations/en.ts b/packages/compliance/src/translations/en.ts index 804d4e2..c37d150 100644 --- a/packages/compliance/src/translations/en.ts +++ b/packages/compliance/src/translations/en.ts @@ -5,17 +5,29 @@ import type { TranslationData } from '@objectstack/spec/system'; export const en: TranslationData = { objects: { compliance_framework: { - label: 'Framework', pluralLabel: 'Frameworks', + label: 'Framework', + pluralLabel: 'Frameworks', description: 'A compliance standard you are certifying against.', fields: { short_name: { label: 'Short Name' }, full_name: { label: 'Full Name' }, family: { label: 'Family', - options: { soc2: 'SOC 2', iso27001: 'ISO 27001 / 27002', hipaa: 'HIPAA', gdpr: 'GDPR', pci: 'PCI DSS', nist_csf: 'NIST CSF', custom: 'Custom' }, + options: { + soc2: 'SOC 2', + iso27001: 'ISO 27001 / 27002', + hipaa: 'HIPAA', + gdpr: 'GDPR', + pci: 'PCI DSS', + nist_csf: 'NIST CSF', + custom: 'Custom', + }, }, version: { label: 'Version' }, - status: { label: 'Status', options: { active: 'Active', adopted: 'Adopted', retired: 'Retired' } }, + status: { + label: 'Status', + options: { active: 'Active', adopted: 'Adopted', retired: 'Retired' }, + }, next_audit_date: { label: 'Next Audit Date' }, auditor: { label: 'External Auditor' }, description: { label: 'Description' }, @@ -23,7 +35,8 @@ export const en: TranslationData = { }, compliance_control: { - label: 'Control', pluralLabel: 'Controls', + label: 'Control', + pluralLabel: 'Controls', description: 'An individual control requirement that needs evidence and periodic assessment.', fields: { code: { label: 'Control Code' }, @@ -31,12 +44,29 @@ export const en: TranslationData = { framework: { label: 'Framework' }, category: { label: 'Category', - options: { access: 'Access Control', change: 'Change Management', risk: 'Risk Management', vendor: 'Vendor Management', incident: 'Incident Response', data: 'Data Protection', physical: 'Physical Security', other: 'Other' }, + options: { + access: 'Access Control', + change: 'Change Management', + risk: 'Risk Management', + vendor: 'Vendor Management', + incident: 'Incident Response', + data: 'Data Protection', + physical: 'Physical Security', + other: 'Other', + }, + }, + criticality: { + label: 'Criticality', + options: { high: 'High', medium: 'Medium', low: 'Low' }, }, - criticality: { label: 'Criticality', options: { high: 'High', medium: 'Medium', low: 'Low' } }, last_status: { label: 'Last Test Result', - options: { not_tested: 'Not Tested', passed: 'Passed', partial: 'Partial', failed: 'Failed' }, + options: { + not_tested: 'Not Tested', + passed: 'Passed', + partial: 'Partial', + failed: 'Failed', + }, }, description: { label: 'Description' }, owner: { label: 'Control Owner' }, @@ -47,26 +77,51 @@ export const en: TranslationData = { }, _views: { all_controls: { label: 'All Controls', description: 'Every control, grouped by framework' }, - control_board: { label: 'Control Board', description: 'Kanban grouped by last test result' }, + control_board: { + label: 'Control Board', + description: 'Kanban grouped by last test result', + }, my_controls: { label: 'My Controls', description: 'Controls you own' }, - failing_controls: { label: 'Failing Controls', description: 'Controls with failed or partial last result' }, - overdue_controls: { label: 'Overdue for Review', description: 'Controls past their review-frequency window' }, + failing_controls: { + label: 'Failing Controls', + description: 'Controls with failed or partial last result', + }, + overdue_controls: { + label: 'Overdue for Review', + description: 'Controls past their review-frequency window', + }, }, }, compliance_evidence: { - label: 'Evidence', pluralLabel: 'Evidence', + label: 'Evidence', + pluralLabel: 'Evidence', description: 'Proof supporting one or more controls.', fields: { title: { label: 'Title' }, control: { label: 'Primary Control' }, evidence_type: { label: 'Type', - options: { policy: 'Policy Document', screenshot: 'Screenshot', log: 'System Log Export', config: 'Configuration Export', training: 'Training Record', pentest: 'Penetration Test Report', audit_letter: 'External Audit Letter', other: 'Other' }, + options: { + policy: 'Policy Document', + screenshot: 'Screenshot', + log: 'System Log Export', + config: 'Configuration Export', + training: 'Training Record', + pentest: 'Penetration Test Report', + audit_letter: 'External Audit Letter', + other: 'Other', + }, }, status: { label: 'Status', - options: { pending: 'Pending', submitted: 'Submitted', approved: 'Approved', rejected: 'Rejected', expired: 'Expired' }, + options: { + pending: 'Pending', + submitted: 'Submitted', + approved: 'Approved', + rejected: 'Rejected', + expired: 'Expired', + }, }, description: { label: 'Description' }, collected_on: { label: 'Collected On' }, @@ -86,7 +141,8 @@ export const en: TranslationData = { }, compliance_assessment: { - label: 'Assessment', pluralLabel: 'Assessments', + label: 'Assessment', + pluralLabel: 'Assessments', description: 'A periodic test of a control.', fields: { title: { label: 'Title' }, @@ -96,7 +152,13 @@ export const en: TranslationData = { assessor: { label: 'Assessor' }, status: { label: 'Status', - options: { planned: 'Planned', in_progress: 'In Progress', passed: 'Passed', partial: 'Partial', failed: 'Failed' }, + options: { + planned: 'Planned', + in_progress: 'In Progress', + passed: 'Passed', + partial: 'Partial', + failed: 'Failed', + }, }, finding: { label: 'Finding' }, remediation_plan: { label: 'Remediation Plan' }, @@ -125,9 +187,14 @@ export const en: TranslationData = { }, messages: { - 'common.save': 'Save', 'common.cancel': 'Cancel', 'common.delete': 'Delete', - 'common.edit': 'Edit', 'common.create': 'Create', 'common.search': 'Search', - 'common.filter': 'Filter', 'common.export': 'Export', + 'common.save': 'Save', + 'common.cancel': 'Cancel', + 'common.delete': 'Delete', + 'common.edit': 'Edit', + 'common.create': 'Create', + 'common.search': 'Search', + 'common.filter': 'Filter', + 'common.export': 'Export', 'success.saved': 'Saved successfully', 'error.required': 'This field is required', }, @@ -146,10 +213,22 @@ export const en: TranslationData = { description: 'Pass rate, failing controls, expiring evidence, and in-flight assessments.', actions: { create_assessment: { label: 'New Assessment' } }, widgets: { - passing_controls: { title: 'Controls Passing', description: 'Controls whose most recent assessment passed' }, - failing_controls: { title: 'Failing or Partial', description: 'Controls whose most recent assessment failed or was partial' }, - expiring_evidence: { title: 'Evidence Expiring ≤ 30d', description: 'Approved evidence with expires_on within 30 days' }, - in_progress_assessments: { title: 'Assessments In Progress', description: 'Assessments currently being conducted' }, + passing_controls: { + title: 'Controls Passing', + description: 'Controls whose most recent assessment passed', + }, + failing_controls: { + title: 'Failing or Partial', + description: 'Controls whose most recent assessment failed or was partial', + }, + expiring_evidence: { + title: 'Evidence Expiring ≤ 30d', + description: 'Approved evidence with expires_on within 30 days', + }, + in_progress_assessments: { + title: 'Assessments In Progress', + description: 'Assessments currently being conducted', + }, failing_table: { title: 'Failing Controls (Action Required)' }, expiring_evidence_table: { title: 'Evidence Expiring Soon' }, }, diff --git a/packages/compliance/src/translations/zh-CN.ts b/packages/compliance/src/translations/zh-CN.ts index 5a0d296..70b1fa9 100644 --- a/packages/compliance/src/translations/zh-CN.ts +++ b/packages/compliance/src/translations/zh-CN.ts @@ -8,17 +8,29 @@ import type { TranslationData } from '@objectstack/spec/system'; export const zhCN: TranslationData = { objects: { compliance_framework: { - label: '合规框架', pluralLabel: '合规框架', + label: '合规框架', + pluralLabel: '合规框架', description: '正在认证或贯标的合规标准。', fields: { short_name: { label: '简称' }, full_name: { label: '全称' }, family: { label: '类别', - options: { soc2: 'SOC 2', iso27001: 'ISO 27001 / 27002', hipaa: 'HIPAA', gdpr: 'GDPR', pci: 'PCI DSS', nist_csf: 'NIST CSF', custom: '自定义' }, + options: { + soc2: 'SOC 2', + iso27001: 'ISO 27001 / 27002', + hipaa: 'HIPAA', + gdpr: 'GDPR', + pci: 'PCI DSS', + nist_csf: 'NIST CSF', + custom: '自定义', + }, }, version: { label: '版本' }, - status: { label: '状态', options: { active: '生效', adopted: '已采用', retired: '已退役' } }, + status: { + label: '状态', + options: { active: '生效', adopted: '已采用', retired: '已退役' }, + }, next_audit_date: { label: '下次审计日期' }, auditor: { label: '外部审计方' }, description: { label: '描述' }, @@ -26,7 +38,8 @@ export const zhCN: TranslationData = { }, compliance_control: { - label: '控制项', pluralLabel: '控制项', + label: '控制项', + pluralLabel: '控制项', description: '单个控制要求,需要佐证材料并定期评估。', fields: { code: { label: '控制项编号' }, @@ -34,7 +47,16 @@ export const zhCN: TranslationData = { framework: { label: '所属框架' }, category: { label: '分类', - options: { access: '访问控制', change: '变更管理', risk: '风险管理', vendor: '供应商管理', incident: '事件响应', data: '数据保护', physical: '物理安全', other: '其他' }, + options: { + access: '访问控制', + change: '变更管理', + risk: '风险管理', + vendor: '供应商管理', + incident: '事件响应', + data: '数据保护', + physical: '物理安全', + other: '其他', + }, }, criticality: { label: '重要等级', options: { high: '高', medium: '中', low: '低' } }, last_status: { @@ -58,18 +80,34 @@ export const zhCN: TranslationData = { }, compliance_evidence: { - label: '佐证材料', pluralLabel: '佐证材料', + label: '佐证材料', + pluralLabel: '佐证材料', description: '支持一项或多项控制项的证据。', fields: { title: { label: '标题' }, control: { label: '主控制项' }, evidence_type: { label: '类型', - options: { policy: '制度文件', screenshot: '截图', log: '日志导出', config: '配置导出', training: '培训记录', pentest: '渗透测试报告', audit_letter: '外部审计函', other: '其他' }, + options: { + policy: '制度文件', + screenshot: '截图', + log: '日志导出', + config: '配置导出', + training: '培训记录', + pentest: '渗透测试报告', + audit_letter: '外部审计函', + other: '其他', + }, }, status: { label: '状态', - options: { pending: '待收集', submitted: '已提交', approved: '已批准', rejected: '已驳回', expired: '已过期' }, + options: { + pending: '待收集', + submitted: '已提交', + approved: '已批准', + rejected: '已驳回', + expired: '已过期', + }, }, description: { label: '描述' }, collected_on: { label: '收集日期' }, @@ -89,7 +127,8 @@ export const zhCN: TranslationData = { }, compliance_assessment: { - label: '评估', pluralLabel: '评估', + label: '评估', + pluralLabel: '评估', description: '对单个控制项的一次定期测试。', fields: { title: { label: '标题' }, @@ -99,7 +138,13 @@ export const zhCN: TranslationData = { assessor: { label: '评估人' }, status: { label: '状态', - options: { planned: '已计划', in_progress: '进行中', passed: '通过', partial: '部分通过', failed: '未通过' }, + options: { + planned: '已计划', + in_progress: '进行中', + passed: '通过', + partial: '部分通过', + failed: '未通过', + }, }, finding: { label: '审计发现' }, remediation_plan: { label: '整改计划' }, @@ -128,9 +173,14 @@ export const zhCN: TranslationData = { }, messages: { - 'common.save': '保存', 'common.cancel': '取消', 'common.delete': '删除', - 'common.edit': '编辑', 'common.create': '新建', 'common.search': '搜索', - 'common.filter': '筛选', 'common.export': '导出', + 'common.save': '保存', + 'common.cancel': '取消', + 'common.delete': '删除', + 'common.edit': '编辑', + 'common.create': '新建', + 'common.search': '搜索', + 'common.filter': '筛选', + 'common.export': '导出', 'success.saved': '保存成功', 'error.required': '此字段必填', }, diff --git a/packages/compliance/src/views/compliance_assessment.view.ts b/packages/compliance/src/views/compliance_assessment.view.ts index f921120..0fd2f33 100644 --- a/packages/compliance/src/views/compliance_assessment.view.ts +++ b/packages/compliance/src/views/compliance_assessment.view.ts @@ -35,9 +35,7 @@ export const AssessmentViews = defineView({ label: 'Failed Assessments', data: { provider: 'object', object: 'compliance_assessment' }, columns: ['title', 'control', 'cycle', 'assessed_at', 'remediation_due', 'assessor'], - filter: [ - { field: 'status', operator: 'in', value: ['failed', 'partial'] }, - ], + filter: [{ field: 'status', operator: 'in', value: ['failed', 'partial'] }], sort: [{ field: 'remediation_due', order: 'asc' }], }, @@ -47,9 +45,7 @@ export const AssessmentViews = defineView({ label: 'In Progress', data: { provider: 'object', object: 'compliance_assessment' }, columns: ['title', 'control', 'cycle', 'assessor'], - filter: [ - { field: 'status', operator: 'equals', value: 'in_progress' }, - ], + filter: [{ field: 'status', operator: 'equals', value: 'in_progress' }], sort: [{ field: 'cycle', order: 'desc' }], }, }, @@ -71,8 +67,7 @@ export const AssessmentViews = defineView({ ], }, { label: 'Finding', columns: 1, fields: ['finding'] }, - { label: 'Remediation', columns: 2, - fields: ['remediation_due', 'remediation_plan'] }, + { label: 'Remediation', columns: 2, fields: ['remediation_due', 'remediation_plan'] }, ], }, }); diff --git a/packages/compliance/src/views/compliance_control.view.ts b/packages/compliance/src/views/compliance_control.view.ts index 027889f..2c8c6fd 100644 --- a/packages/compliance/src/views/compliance_control.view.ts +++ b/packages/compliance/src/views/compliance_control.view.ts @@ -51,9 +51,7 @@ export const ControlViews = defineView({ label: 'My Controls', data: { provider: 'object', object: 'compliance_control' }, columns: ['code', 'title', 'framework', 'criticality', 'last_status', 'last_assessed_at'], - filter: [ - { field: 'owner', operator: 'equals', value: '{current_user_id}' }, - ], + filter: [{ field: 'owner', operator: 'equals', value: '{current_user_id}' }], sort: [{ field: 'code', order: 'asc' }], }, @@ -63,10 +61,11 @@ export const ControlViews = defineView({ label: 'Failing Controls', data: { provider: 'object', object: 'compliance_control' }, columns: ['code', 'title', 'framework', 'criticality', 'owner', 'last_assessed_at'], - filter: [ - { field: 'last_status', operator: 'in', value: ['failed', 'partial'] }, + filter: [{ field: 'last_status', operator: 'in', value: ['failed', 'partial'] }], + sort: [ + { field: 'criticality', order: 'asc' }, + { field: 'code', order: 'asc' }, ], - sort: [{ field: 'criticality', order: 'asc' }, { field: 'code', order: 'asc' }], }, overdue_controls: { @@ -75,9 +74,7 @@ export const ControlViews = defineView({ label: 'Overdue for Review', data: { provider: 'object', object: 'compliance_control' }, columns: ['code', 'title', 'framework', 'criticality', 'owner', 'last_assessed_at'], - filter: [ - { field: 'is_overdue_for_review', operator: 'equals', value: true }, - ], + filter: [{ field: 'is_overdue_for_review', operator: 'equals', value: true }], sort: [{ field: 'criticality', order: 'asc' }], }, }, diff --git a/packages/compliance/src/views/compliance_evidence.view.ts b/packages/compliance/src/views/compliance_evidence.view.ts index 34c5f1e..dff8196 100644 --- a/packages/compliance/src/views/compliance_evidence.view.ts +++ b/packages/compliance/src/views/compliance_evidence.view.ts @@ -37,9 +37,7 @@ export const EvidenceViews = defineView({ label: 'Pending Review', data: { provider: 'object', object: 'compliance_evidence' }, columns: ['title', 'control', 'evidence_type', 'collected_on', 'collected_by'], - filter: [ - { field: 'status', operator: 'equals', value: 'submitted' }, - ], + filter: [{ field: 'status', operator: 'equals', value: 'submitted' }], sort: [{ field: 'collected_on', order: 'asc' }], }, @@ -76,8 +74,14 @@ export const EvidenceViews = defineView({ { label: 'Lifecycle', columns: 2, - fields: ['collected_on', 'expires_on', 'collected_by', 'approved_by', - 'is_expiring_soon', 'is_expired'], + fields: [ + 'collected_on', + 'expires_on', + 'collected_by', + 'approved_by', + 'is_expiring_soon', + 'is_expired', + ], }, { label: 'Source / Notes', columns: 1, fields: ['source_url', 'notes'] }, ], diff --git a/packages/content/objectstack.manifest.json b/packages/content/objectstack.manifest.json index f436187..003fc2f 100644 --- a/packages/content/objectstack.manifest.json +++ b/packages/content/objectstack.manifest.json @@ -13,7 +13,12 @@ "iconUrl": "https://cdn.objectos.app/icons/content.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/content", "tags": ["content", "marketing", "editorial", "roi"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md", "translations": { "zh-CN": { @@ -23,4 +28,3 @@ } } } - diff --git a/packages/content/src/actions/publish_now.action.ts b/packages/content/src/actions/publish_now.action.ts index 56189e4..6a40175 100644 --- a/packages/content/src/actions/publish_now.action.ts +++ b/packages/content/src/actions/publish_now.action.ts @@ -46,8 +46,7 @@ export async function publishNow( ); } - const channels = - input.channelIds ?? ((piece.target_channels as string[] | undefined) ?? []); + const channels = input.channelIds ?? (piece.target_channels as string[] | undefined) ?? []; if (channels.length === 0) { throw new Error('Piece has no target_channels and no channelIds were supplied.'); } diff --git a/packages/content/src/actions/suggest_cta.action.ts b/packages/content/src/actions/suggest_cta.action.ts index 560ff9a..e1ec0e3 100644 --- a/packages/content/src/actions/suggest_cta.action.ts +++ b/packages/content/src/actions/suggest_cta.action.ts @@ -84,13 +84,10 @@ export async function suggestCta( ctx: SuggestCtaContext, ): Promise { const piece = await ctx.loadPiece(input.pieceId); - const topic = ctx.loadTopic && piece.topic - ? await ctx.loadTopic(String(piece.topic)) - : undefined; + const topic = ctx.loadTopic && piece.topic ? await ctx.loadTopic(String(piece.topic)) : undefined; const targetChannelIds = (piece.target_channels as string[] | undefined) ?? []; - const channel = ctx.loadChannel && targetChannelIds[0] - ? await ctx.loadChannel(targetChannelIds[0]) - : undefined; + const channel = + ctx.loadChannel && targetChannelIds[0] ? await ctx.loadChannel(targetChannelIds[0]) : undefined; const promptCtx = `PIECE TITLE: ${piece.title ?? ''} SUMMARY: ${piece.summary ?? ''} diff --git a/packages/content/src/dashboards/editorial_calendar.dashboard.ts b/packages/content/src/dashboards/editorial_calendar.dashboard.ts index e4ffe99..978a70b 100644 --- a/packages/content/src/dashboards/editorial_calendar.dashboard.ts +++ b/packages/content/src/dashboards/editorial_calendar.dashboard.ts @@ -24,9 +24,7 @@ export const EditorialCalendarDashboard: Dashboard = { header: { showTitle: true, showDescription: true, - actions: [ - { label: 'New Piece', icon: 'Plus', actionType: 'modal', actionUrl: 'create_piece' }, - ], + actions: [{ label: 'New Piece', icon: 'Plus', actionType: 'modal', actionUrl: 'create_piece' }], }, widgets: [ diff --git a/packages/content/src/dashboards/roi_by_channel.dashboard.ts b/packages/content/src/dashboards/roi_by_channel.dashboard.ts index 8040d8b..a4734fa 100644 --- a/packages/content/src/dashboards/roi_by_channel.dashboard.ts +++ b/packages/content/src/dashboards/roi_by_channel.dashboard.ts @@ -123,10 +123,18 @@ export const RoiByChannelDashboard: Dashboard = { type: 'table', object: 'content_publication', aggregate: 'count', - + layout: { x: 0, y: 7, w: 12, h: 5 }, options: { - columns: ['piece', 'channel', 'published_at', 'total_views', 'total_clicks', 'total_signups', 'total_revenue'], + columns: [ + 'piece', + 'channel', + 'published_at', + 'total_views', + 'total_clicks', + 'total_signups', + 'total_revenue', + ], pageSize: 10, sort: [{ field: 'total_signups', order: 'desc' }], }, diff --git a/packages/content/src/dashboards/today_workbench.dashboard.ts b/packages/content/src/dashboards/today_workbench.dashboard.ts index d6617e5..2c071fc 100644 --- a/packages/content/src/dashboards/today_workbench.dashboard.ts +++ b/packages/content/src/dashboards/today_workbench.dashboard.ts @@ -13,7 +13,8 @@ import type { Dashboard } from '@objectstack/spec/ui'; export const TodayWorkbenchDashboard: Dashboard = { name: 'today_workbench_dashboard', label: 'Today Workbench', - description: 'Your shift on the editorial floor: drafts in flight, what is pending review, and signals to triage.', + description: + 'Your shift on the editorial floor: drafts in flight, what is pending review, and signals to triage.', columns: 12, gap: 4, @@ -22,9 +23,7 @@ export const TodayWorkbenchDashboard: Dashboard = { header: { showTitle: true, showDescription: true, - actions: [ - { label: 'New Piece', icon: 'Plus', actionType: 'modal', actionUrl: 'create_piece' }, - ], + actions: [{ label: 'New Piece', icon: 'Plus', actionType: 'modal', actionUrl: 'create_piece' }], }, widgets: [ diff --git a/packages/content/src/data/index.ts b/packages/content/src/data/index.ts index f402b23..91cf1bd 100644 --- a/packages/content/src/data/index.ts +++ b/packages/content/src/data/index.ts @@ -212,7 +212,8 @@ const signals = defineDataset(Signal, { captured_at: cel`daysAgo(10)`, status: 'promoted', impact: 'medium', - summary: 'Multiple +1s. Two paying customers chimed in. Cheap to ship as a small RSS endpoint.', + summary: + 'Multiple +1s. Two paying customers chimed in. Cheap to ship as a small RSS endpoint.', recommended_topic_title: 'Introducing the changelog RSS feed', promoted_at: cel`daysAgo(9)`, }, @@ -686,20 +687,118 @@ const ctas = defineDataset(Cta, { externalId: 'variant', records: [ // One CTA per piece — variant tag includes the piece slug so externalId is unique. - { piece: 'Honest comparison: us vs Helio', label_text: 'Start free trial', goal: 'signup', destination_url: 'https://example.com/signup?utm_content=us-vs-helio', variant: 'us-vs-helio-default', is_primary: true }, - { piece: 'A short anatomy of a great launch week', label_text: 'Subscribe for launch recaps', goal: 'subscribe', destination_url: 'https://example.com/newsletter?utm_content=launch-week', variant: 'launch-week-default', is_primary: true }, - { piece: 'Five anti-patterns we keep seeing in dashboards', label_text: 'Audit your dashboard', goal: 'demo', destination_url: 'https://example.com/audit?utm_content=anti-patterns', variant: 'anti-patterns-default', is_primary: true }, - { piece: 'The marketing metrics dashboard we actually use', label_text: 'See the template', goal: 'read', destination_url: 'https://example.com/templates/metrics?utm_content=mkt-dash', variant: 'mkt-dash-default', is_primary: true }, - { piece: 'Picking the right region — a 3-minute guide', label_text: 'Open docs', goal: 'read', destination_url: 'https://docs.example.com/regions?utm_content=region-guide', variant: 'region-guide-default', is_primary: true }, - { piece: 'Customer story: Northwind cut report time from 3 days to 4 hours', label_text: 'Book a demo', goal: 'demo', destination_url: 'https://example.com/demo?utm_content=northwind', variant: 'northwind-default', is_primary: true }, - { piece: 'How our SOC 2 program actually works (and where the report lives)', label_text: 'Book a security review', goal: 'demo', destination_url: 'https://example.com/security-review?utm_content=soc2', variant: 'soc2-default', is_primary: true }, - { piece: 'What it actually costs to run our analytics stack', label_text: 'Get the cost calculator', goal: 'signup', destination_url: 'https://example.com/cost-calculator?utm_content=stack-cost', variant: 'stack-cost-default', is_primary: true }, - { piece: 'Introducing the changelog RSS feed', label_text: 'Add the feed', goal: 'subscribe', destination_url: 'https://example.com/changelog.xml', variant: 'changelog-default', is_primary: true }, - { piece: 'Weekly: Issue #42 — Two ways to think about region selection', label_text: 'Read the full guide', goal: 'read', destination_url: 'https://example.com/blog/choose-region', variant: 'newsletter-42-default', is_primary: true }, - { piece: 'Weekly: Issue #43 — What we shipped this week', label_text: 'See the changelog', goal: 'read', destination_url: 'https://example.com/changelog', variant: 'newsletter-43-default', is_primary: true }, - { piece: 'Why we kept the demo gate (and when we will drop it)', label_text: 'Start free trial', goal: 'signup', destination_url: 'https://example.com/signup?utm_content=demo-gate', variant: 'demo-gate-default', is_primary: true }, - { piece: 'How we decide what makes the public roadmap', label_text: 'See the roadmap', goal: 'read', destination_url: 'https://example.com/roadmap?utm_content=criteria-post', variant: 'roadmap-default', is_primary: true }, - { piece: 'Q3 2025 metrics: what worked, what didn\u2019t', label_text: 'Subscribe to Q4 recap', goal: 'subscribe', destination_url: 'https://example.com/newsletter?utm_content=q3-recap', variant: 'q3-default', is_primary: true }, + { + piece: 'Honest comparison: us vs Helio', + label_text: 'Start free trial', + goal: 'signup', + destination_url: 'https://example.com/signup?utm_content=us-vs-helio', + variant: 'us-vs-helio-default', + is_primary: true, + }, + { + piece: 'A short anatomy of a great launch week', + label_text: 'Subscribe for launch recaps', + goal: 'subscribe', + destination_url: 'https://example.com/newsletter?utm_content=launch-week', + variant: 'launch-week-default', + is_primary: true, + }, + { + piece: 'Five anti-patterns we keep seeing in dashboards', + label_text: 'Audit your dashboard', + goal: 'demo', + destination_url: 'https://example.com/audit?utm_content=anti-patterns', + variant: 'anti-patterns-default', + is_primary: true, + }, + { + piece: 'The marketing metrics dashboard we actually use', + label_text: 'See the template', + goal: 'read', + destination_url: 'https://example.com/templates/metrics?utm_content=mkt-dash', + variant: 'mkt-dash-default', + is_primary: true, + }, + { + piece: 'Picking the right region — a 3-minute guide', + label_text: 'Open docs', + goal: 'read', + destination_url: 'https://docs.example.com/regions?utm_content=region-guide', + variant: 'region-guide-default', + is_primary: true, + }, + { + piece: 'Customer story: Northwind cut report time from 3 days to 4 hours', + label_text: 'Book a demo', + goal: 'demo', + destination_url: 'https://example.com/demo?utm_content=northwind', + variant: 'northwind-default', + is_primary: true, + }, + { + piece: 'How our SOC 2 program actually works (and where the report lives)', + label_text: 'Book a security review', + goal: 'demo', + destination_url: 'https://example.com/security-review?utm_content=soc2', + variant: 'soc2-default', + is_primary: true, + }, + { + piece: 'What it actually costs to run our analytics stack', + label_text: 'Get the cost calculator', + goal: 'signup', + destination_url: 'https://example.com/cost-calculator?utm_content=stack-cost', + variant: 'stack-cost-default', + is_primary: true, + }, + { + piece: 'Introducing the changelog RSS feed', + label_text: 'Add the feed', + goal: 'subscribe', + destination_url: 'https://example.com/changelog.xml', + variant: 'changelog-default', + is_primary: true, + }, + { + piece: 'Weekly: Issue #42 — Two ways to think about region selection', + label_text: 'Read the full guide', + goal: 'read', + destination_url: 'https://example.com/blog/choose-region', + variant: 'newsletter-42-default', + is_primary: true, + }, + { + piece: 'Weekly: Issue #43 — What we shipped this week', + label_text: 'See the changelog', + goal: 'read', + destination_url: 'https://example.com/changelog', + variant: 'newsletter-43-default', + is_primary: true, + }, + { + piece: 'Why we kept the demo gate (and when we will drop it)', + label_text: 'Start free trial', + goal: 'signup', + destination_url: 'https://example.com/signup?utm_content=demo-gate', + variant: 'demo-gate-default', + is_primary: true, + }, + { + piece: 'How we decide what makes the public roadmap', + label_text: 'See the roadmap', + goal: 'read', + destination_url: 'https://example.com/roadmap?utm_content=criteria-post', + variant: 'roadmap-default', + is_primary: true, + }, + { + piece: 'Q3 2025 metrics: what worked, what didn\u2019t', + label_text: 'Subscribe to Q4 recap', + goal: 'subscribe', + destination_url: 'https://example.com/newsletter?utm_content=q3-recap', + variant: 'q3-default', + is_primary: true, + }, ], }); diff --git a/packages/content/src/objects/content_metric.object.ts b/packages/content/src/objects/content_metric.object.ts index 9da0b49..e2e255d 100644 --- a/packages/content/src/objects/content_metric.object.ts +++ b/packages/content/src/objects/content_metric.object.ts @@ -61,11 +61,7 @@ export const Metric = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['publication'] }, - { fields: ['period_start'] }, - { fields: ['period_end'] }, - ], + indexes: [{ fields: ['publication'] }, { fields: ['period_start'] }, { fields: ['period_end'] }], titleFormat: tmpl`{{record.publication}} — {{record.period_start}}`, compactLayout: ['publication', 'period_start', 'period_end', 'views', 'signups', 'revenue'], diff --git a/packages/content/src/objects/content_piece.object.ts b/packages/content/src/objects/content_piece.object.ts index b7de73c..1161357 100644 --- a/packages/content/src/objects/content_piece.object.ts +++ b/packages/content/src/objects/content_piece.object.ts @@ -219,7 +219,16 @@ export const Piece = ObjectSchema.create({ type: 'state_machine', name: 'content_piece_lifecycle', field: 'status', - transitions: {backlog:["drafting", "cancelled"], drafting:["in_review", "backlog", "cancelled"], in_review:["approved", "drafting", "cancelled"], approved:["scheduled", "drafting", "cancelled"], scheduled:["published", "approved", "cancelled"], published:["archived"], archived:[], cancelled:[]}, + transitions: { + backlog: ['drafting', 'cancelled'], + drafting: ['in_review', 'backlog', 'cancelled'], + in_review: ['approved', 'drafting', 'cancelled'], + approved: ['scheduled', 'drafting', 'cancelled'], + scheduled: ['published', 'approved', 'cancelled'], + published: ['archived'], + archived: [], + cancelled: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/content/src/objects/content_publication.object.ts b/packages/content/src/objects/content_publication.object.ts index 1ef211a..24629e6 100644 --- a/packages/content/src/objects/content_publication.object.ts +++ b/packages/content/src/objects/content_publication.object.ts @@ -107,11 +107,7 @@ export const Publication = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['piece'] }, - { fields: ['channel'] }, - { fields: ['published_at'] }, - ], + indexes: [{ fields: ['piece'] }, { fields: ['channel'] }, { fields: ['published_at'] }], titleFormat: tmpl`{{record.piece}} on {{record.channel}}`, compactLayout: ['piece', 'channel', 'published_at', 'total_views', 'total_signups'], diff --git a/packages/content/src/objects/content_signal.object.ts b/packages/content/src/objects/content_signal.object.ts index 304bb8b..0863b77 100644 --- a/packages/content/src/objects/content_signal.object.ts +++ b/packages/content/src/objects/content_signal.object.ts @@ -146,7 +146,7 @@ export const Signal = ObjectSchema.create({ type: 'state_machine', name: 'content_signal_lifecycle', field: 'status', - transitions: {captured:["promoted", "ignored"], promoted:[], ignored:[]}, + transitions: { captured: ['promoted', 'ignored'], promoted: [], ignored: [] }, message: 'Illegal status transition.', }, { diff --git a/packages/content/src/translations/en.ts b/packages/content/src/translations/en.ts index aefe4f5..25904cc 100644 --- a/packages/content/src/translations/en.ts +++ b/packages/content/src/translations/en.ts @@ -134,8 +134,14 @@ export const en: TranslationData = { }, _views: { all_signals: { label: 'All Signals', description: 'Every captured signal' }, - my_triage_queue: { label: 'My Triage', description: 'Captured signals awaiting your decision' }, - recently_promoted: { label: 'Recently Promoted', description: 'Signals promoted in the last 30 days' }, + my_triage_queue: { + label: 'My Triage', + description: 'Captured signals awaiting your decision', + }, + recently_promoted: { + label: 'Recently Promoted', + description: 'Signals promoted in the last 30 days', + }, }, }, @@ -158,7 +164,11 @@ export const en: TranslationData = { }, funnel_stage: { label: 'Funnel Stage', - options: { tofu: 'Awareness (TOFU)', mofu: 'Consideration (MOFU)', bofu: 'Decision (BOFU)' }, + options: { + tofu: 'Awareness (TOFU)', + mofu: 'Consideration (MOFU)', + bofu: 'Decision (BOFU)', + }, }, priority: { label: 'Priority', @@ -234,12 +244,24 @@ export const en: TranslationData = { _views: { all_pieces: { label: 'All Pieces', description: 'Every piece grouped by status' }, piece_board: { label: 'Pipeline', description: 'Kanban grouped by status' }, - my_drafts: { label: 'My Drafts', description: 'Pieces assigned to you and not yet in review' }, + my_drafts: { + label: 'My Drafts', + description: 'Pieces assigned to you and not yet in review', + }, in_review_queue: { label: 'In Review', description: 'Pieces awaiting editorial sign-off' }, editorial_calendar: { label: 'Calendar', description: 'Pieces by publish date' }, - scheduled_pieces: { label: 'Scheduled', description: 'Approved pieces with a scheduled time' }, - published_pieces: { label: 'Published (30d)', description: 'Pieces that went live in the last 30 days' }, - top_performers: { label: 'Top Performers', description: 'Published pieces sorted by total views' }, + scheduled_pieces: { + label: 'Scheduled', + description: 'Approved pieces with a scheduled time', + }, + published_pieces: { + label: 'Published (30d)', + description: 'Pieces that went live in the last 30 days', + }, + top_performers: { + label: 'Top Performers', + description: 'Published pieces sorted by total views', + }, }, }, @@ -261,9 +283,18 @@ export const en: TranslationData = { notes: { label: 'Notes' }, }, _views: { - all_publications: { label: 'All Publications', description: 'Every publication, latest first' }, - this_week_publications: { label: 'This Week', description: 'Publications in the current week' }, - by_channel_publications: { label: 'By Channel', description: 'Publications grouped by channel' }, + all_publications: { + label: 'All Publications', + description: 'Every publication, latest first', + }, + this_week_publications: { + label: 'This Week', + description: 'Publications in the current week', + }, + by_channel_publications: { + label: 'By Channel', + description: 'Publications grouped by channel', + }, }, }, @@ -377,12 +408,30 @@ export const en: TranslationData = { capture_signal: { label: 'Capture Signal' }, }, widgets: { - my_drafts_in_flight: { title: 'My Drafts In Flight', description: 'Pieces assigned to you in drafting status' }, - my_pieces_in_review: { title: 'My Pieces In Review', description: 'Pieces you submitted that are in review' }, - scheduled_this_week: { title: 'Publishing This Week', description: 'Scheduled pieces with publish date this week' }, - published_last_7d: { title: 'Published (Last 7d)', description: 'Pieces published in the last 7 days' }, - my_pieces_table: { title: 'My Recent Pieces', description: 'Your in-flight pieces, latest activity first' }, - signals_to_triage: { title: 'Signals to Triage', description: 'Captured signals not yet promoted or ignored' }, + my_drafts_in_flight: { + title: 'My Drafts In Flight', + description: 'Pieces assigned to you in drafting status', + }, + my_pieces_in_review: { + title: 'My Pieces In Review', + description: 'Pieces you submitted that are in review', + }, + scheduled_this_week: { + title: 'Publishing This Week', + description: 'Scheduled pieces with publish date this week', + }, + published_last_7d: { + title: 'Published (Last 7d)', + description: 'Pieces published in the last 7 days', + }, + my_pieces_table: { + title: 'My Recent Pieces', + description: 'Your in-flight pieces, latest activity first', + }, + signals_to_triage: { + title: 'Signals to Triage', + description: 'Captured signals not yet promoted or ignored', + }, }, }, editorial_calendar_dashboard: { @@ -394,10 +443,22 @@ export const en: TranslationData = { widgets: { pieces_scheduled: { title: 'Scheduled', description: 'Pieces in scheduled status' }, pieces_in_review: { title: 'Awaiting Review', description: 'Pieces in review' }, - pieces_published_30d: { title: 'Published (30d)', description: 'Pieces published in the last 30 days' }, - pieces_overdue: { title: 'Overdue', description: 'Pieces where publish_at has passed without going live' }, - calendar_main: { title: 'Upcoming Pieces', description: 'Pieces with publish_at in the next 30 days' }, - publications_by_channel: { title: 'Channel Mix (Recent)', description: 'Distribution of recent publications by channel' }, + pieces_published_30d: { + title: 'Published (30d)', + description: 'Pieces published in the last 30 days', + }, + pieces_overdue: { + title: 'Overdue', + description: 'Pieces where publish_at has passed without going live', + }, + calendar_main: { + title: 'Upcoming Pieces', + description: 'Pieces with publish_at in the next 30 days', + }, + publications_by_channel: { + title: 'Channel Mix (Recent)', + description: 'Distribution of recent publications by channel', + }, }, }, roi_by_channel_dashboard: { @@ -405,13 +466,31 @@ export const en: TranslationData = { description: 'Exec view of return on content investment per channel.', actions: {}, widgets: { - total_views_90d: { title: 'Total Views', description: 'Sum of total_views across all publications' }, - total_clicks_90d: { title: 'Total Clicks', description: 'Sum of total_clicks across all publications' }, - total_signups_90d: { title: 'Total Signups', description: 'Sum of total_signups across all publications' }, - total_revenue_90d: { title: 'Attributed Revenue', description: 'Sum of attributed_revenue across all publications' }, - views_by_channel_bar: { title: 'Views by Channel', description: 'Per-channel sum of views across recent publications' }, + total_views_90d: { + title: 'Total Views', + description: 'Sum of total_views across all publications', + }, + total_clicks_90d: { + title: 'Total Clicks', + description: 'Sum of total_clicks across all publications', + }, + total_signups_90d: { + title: 'Total Signups', + description: 'Sum of total_signups across all publications', + }, + total_revenue_90d: { + title: 'Attributed Revenue', + description: 'Sum of attributed_revenue across all publications', + }, + views_by_channel_bar: { + title: 'Views by Channel', + description: 'Per-channel sum of views across recent publications', + }, signups_trend: { title: 'Signups Trend (90d)', description: 'Daily signups, last 90 days' }, - top_publications: { title: 'Top Publications (90d)', description: 'Highest-revenue publications in the last 90 days' }, + top_publications: { + title: 'Top Publications (90d)', + description: 'Highest-revenue publications in the last 90 days', + }, }, }, }, diff --git a/packages/content/src/translations/zh-CN.ts b/packages/content/src/translations/zh-CN.ts index 3cc3bd9..9db5637 100644 --- a/packages/content/src/translations/zh-CN.ts +++ b/packages/content/src/translations/zh-CN.ts @@ -393,7 +393,10 @@ export const zhCN: TranslationData = { pieces_published_30d: { title: '近 30 天已发布', description: '过去 30 天内发布的内容' }, pieces_overdue: { title: '已逾期', description: '发布时间已过但尚未发布的内容' }, calendar_main: { title: '即将发布的内容', description: '发布时间在未来 30 天内的内容' }, - publications_by_channel: { title: '渠道分布(近期)', description: '按渠道统计的近期发布记录' }, + publications_by_channel: { + title: '渠道分布(近期)', + description: '按渠道统计的近期发布记录', + }, }, }, roi_by_channel_dashboard: { @@ -407,7 +410,10 @@ export const zhCN: TranslationData = { total_revenue_90d: { title: '累计收入', description: '所有发布渠道的归因收入总和' }, views_by_channel_bar: { title: '各渠道浏览', description: '按渠道汇总的浏览数' }, signups_trend: { title: '注册趋势(近 90 天)', description: '每日注册数,近 90 天' }, - top_publications: { title: 'Top 发布(近 90 天)', description: '近 90 天收入最高的发布记录' }, + top_publications: { + title: 'Top 发布(近 90 天)', + description: '近 90 天收入最高的发布记录', + }, }, }, }, diff --git a/packages/content/src/views/content_signal.view.ts b/packages/content/src/views/content_signal.view.ts index 1a19a7d..1f6e866 100644 --- a/packages/content/src/views/content_signal.view.ts +++ b/packages/content/src/views/content_signal.view.ts @@ -27,7 +27,12 @@ export const SignalViews = defineView({ tabs: [ { name: 'all', label: 'All', view: 'all_signals', isDefault: true, pinned: true }, { name: 'triage', label: 'My Triage', icon: 'inbox', view: 'my_triage_queue' }, - { name: 'promoted', label: 'Recently Promoted', icon: 'trending-up', view: 'recently_promoted' }, + { + name: 'promoted', + label: 'Recently Promoted', + icon: 'trending-up', + view: 'recently_promoted', + }, ], }, diff --git a/packages/content/src/views/index.ts b/packages/content/src/views/index.ts index b7a91a7..83c9336 100644 --- a/packages/content/src/views/index.ts +++ b/packages/content/src/views/index.ts @@ -3,9 +3,4 @@ export { PieceViews } from './content_piece.view'; export { SignalViews } from './content_signal.view'; export { PublicationViews } from './content_publication.view'; -export { - TopicViews, - CompetitorViews, - ChannelViews, - TemplateViews, -} from './content_reference.view'; +export { TopicViews, CompetitorViews, ChannelViews, TemplateViews } from './content_reference.view'; diff --git a/packages/contracts/objectstack.manifest.json b/packages/contracts/objectstack.manifest.json index dbc38f4..e918716 100644 --- a/packages/contracts/objectstack.manifest.json +++ b/packages/contracts/objectstack.manifest.json @@ -13,7 +13,13 @@ "iconUrl": "https://cdn.objectos.app/icons/contracts.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/contracts", "tags": ["clm", "contracts", "legal", "approvals"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation", "objectstack-ai"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation", + "objectstack-ai" + ], "readmePath": "README.md", "translations": { "zh-CN": { @@ -23,4 +29,3 @@ } } } - diff --git a/packages/contracts/src/actions/extract_terms.action.ts b/packages/contracts/src/actions/extract_terms.action.ts index bf04ff5..b130bfb 100644 --- a/packages/contracts/src/actions/extract_terms.action.ts +++ b/packages/contracts/src/actions/extract_terms.action.ts @@ -48,16 +48,7 @@ export interface ExtractTermsInput { export interface ExtractedTerms { party_legal_name: string | null; - contract_type: - | 'nda' - | 'msa' - | 'sow' - | 'dpa' - | 'vendor' - | 'employment' - | 'lease' - | 'other' - | null; + contract_type: 'nda' | 'msa' | 'sow' | 'dpa' | 'vendor' | 'employment' | 'lease' | 'other' | null; total_value: number | null; currency: string | null; effective_date: string | null; // ISO date @@ -171,10 +162,7 @@ export async function runExtraction( export interface ExtractTermsContext { loadContractPdfText: (contractId: string) => Promise; loadContract: (contractId: string) => Promise>; - updateContract: ( - contractId: string, - patch: Record, - ) => Promise; + updateContract: (contractId: string, patch: Record) => Promise; } export async function extractTerms( diff --git a/packages/contracts/src/flows/index.ts b/packages/contracts/src/flows/index.ts index d720456..64da9fc 100644 --- a/packages/contracts/src/flows/index.ts +++ b/packages/contracts/src/flows/index.ts @@ -4,8 +4,4 @@ import { ContractRenewalAlertFlow } from './renewal_alert.flow'; import { ContractRenewalDraftFlow } from './renewal_draft.flow'; import { ObligationOverdueFlow } from './obligation_overdue.flow'; -export const allFlows = [ - ContractRenewalAlertFlow, - ContractRenewalDraftFlow, - ObligationOverdueFlow, -]; +export const allFlows = [ContractRenewalAlertFlow, ContractRenewalDraftFlow, ObligationOverdueFlow]; diff --git a/packages/contracts/src/objects/contracts_contract.object.ts b/packages/contracts/src/objects/contracts_contract.object.ts index d480bde..cf89021 100644 --- a/packages/contracts/src/objects/contracts_contract.object.ts +++ b/packages/contracts/src/objects/contracts_contract.object.ts @@ -134,7 +134,8 @@ export const Contract = ObjectSchema.create({ scale: 0, min: 0, group: 'renewal', - description: 'Days before end_date by which we must notify to cancel. Drives alert lead time.', + description: + 'Days before end_date by which we must notify to cancel. Drives alert lead time.', }), // Derived signals @@ -193,7 +194,8 @@ export const Contract = ObjectSchema.create({ label: 'Tags', maxLength: 200, group: 'meta', - description: 'Comma-separated free tags. Replace with a proper junction in your fork if needed.', + description: + 'Comma-separated free tags. Replace with a proper junction in your fork if needed.', }), notes: Field.markdown({ @@ -229,7 +231,15 @@ export const Contract = ObjectSchema.create({ type: 'state_machine', name: 'contract_lifecycle', field: 'status', - transitions: {draft:["in_review", "cancelled"], in_review:["signed", "draft", "cancelled"], signed:["active", "terminated"], active:["expired", "terminated", "active"], expired:[], terminated:[], cancelled:[]}, + transitions: { + draft: ['in_review', 'cancelled'], + in_review: ['signed', 'draft', 'cancelled'], + signed: ['active', 'terminated'], + active: ['expired', 'terminated', 'active'], + expired: [], + terminated: [], + cancelled: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/contracts/src/objects/contracts_party.object.ts b/packages/contracts/src/objects/contracts_party.object.ts index 94bcee2..c45754f 100644 --- a/packages/contracts/src/objects/contracts_party.object.ts +++ b/packages/contracts/src/objects/contracts_party.object.ts @@ -91,10 +91,7 @@ export const Party = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['legal_name'], unique: true }, - { fields: ['party_type'] }, - ], + indexes: [{ fields: ['legal_name'], unique: true }, { fields: ['party_type'] }], compactLayout: ['legal_name', 'party_type', 'country', 'primary_contact_email'], displayNameField: 'legal_name', diff --git a/packages/contracts/src/translations/en.ts b/packages/contracts/src/translations/en.ts index 0674236..433dd5e 100644 --- a/packages/contracts/src/translations/en.ts +++ b/packages/contracts/src/translations/en.ts @@ -76,9 +76,18 @@ export const en: TranslationData = { _views: { all_contracts: { label: 'All Contracts', description: 'Every contract, grouped by status' }, contract_pipeline: { label: 'Contract Pipeline', description: 'Kanban grouped by status' }, - my_contracts: { label: 'My Contracts', description: 'Contracts you own that are still open' }, - expiring_contracts: { label: 'Expiring Soon', description: 'Active contracts ending in ≤ 60 days' }, - pending_approval_contracts: { label: 'Pending Approval', description: 'Contracts in review awaiting approval' }, + my_contracts: { + label: 'My Contracts', + description: 'Contracts you own that are still open', + }, + expiring_contracts: { + label: 'Expiring Soon', + description: 'Active contracts ending in ≤ 60 days', + }, + pending_approval_contracts: { + label: 'Pending Approval', + description: 'Contracts in review awaiting approval', + }, }, }, @@ -140,9 +149,18 @@ export const en: TranslationData = { notes: { label: 'Notes' }, }, _views: { - all_obligations: { label: 'All Obligations', description: 'Every obligation, grouped by status' }, - my_open_obligations: { label: 'My Open Obligations', description: 'Open obligations assigned to you' }, - overdue_obligations: { label: 'Overdue Obligations', description: 'Open obligations past due date' }, + all_obligations: { + label: 'All Obligations', + description: 'Every obligation, grouped by status', + }, + my_open_obligations: { + label: 'My Open Obligations', + description: 'Open obligations assigned to you', + }, + overdue_obligations: { + label: 'Overdue Obligations', + description: 'Open obligations past due date', + }, }, }, }, @@ -194,12 +212,30 @@ export const en: TranslationData = { create_contract: { label: 'New Contract' }, }, widgets: { - expiring_60: { title: 'Expiring ≤ 60 days', description: 'Active contracts with end_date within 60 days' }, - auto_renewing_30: { title: 'Auto-Renewing ≤ 30d', description: 'Active contracts set to auto-renew in 30 days or less' }, - pending_approval: { title: 'Pending Approval', description: 'In-review contracts above the approval threshold' }, - active_total_value: { title: 'Active Portfolio Value', description: 'Sum of total_value across active contracts' }, - expiring_table: { title: 'Expiring Contracts (Next 60d)', description: 'Active contracts sorted by end date — earliest first' }, - pending_obligations: { title: 'Open Obligations', description: 'All open obligations sorted by due date' }, + expiring_60: { + title: 'Expiring ≤ 60 days', + description: 'Active contracts with end_date within 60 days', + }, + auto_renewing_30: { + title: 'Auto-Renewing ≤ 30d', + description: 'Active contracts set to auto-renew in 30 days or less', + }, + pending_approval: { + title: 'Pending Approval', + description: 'In-review contracts above the approval threshold', + }, + active_total_value: { + title: 'Active Portfolio Value', + description: 'Sum of total_value across active contracts', + }, + expiring_table: { + title: 'Expiring Contracts (Next 60d)', + description: 'Active contracts sorted by end date — earliest first', + }, + pending_obligations: { + title: 'Open Obligations', + description: 'All open obligations sorted by due date', + }, }, }, }, diff --git a/packages/contracts/src/translations/zh-CN.ts b/packages/contracts/src/translations/zh-CN.ts index 8c3e84e..cbbcd42 100644 --- a/packages/contracts/src/translations/zh-CN.ts +++ b/packages/contracts/src/translations/zh-CN.ts @@ -13,7 +13,8 @@ export const zhCN: TranslationData = { contracts_contract: { label: '合同', pluralLabel: '合同', - description: '与单一交易对手签署(或谈判中)的协议。在「文件」标签中上传签署版 PDF,然后调用 extract_terms 自动填充元数据。', + description: + '与单一交易对手签署(或谈判中)的协议。在「文件」标签中上传签署版 PDF,然后调用 extract_terms 自动填充元数据。', fields: { title: { label: '标题' }, contract_number: { label: '合同编号' }, @@ -196,7 +197,10 @@ export const zhCN: TranslationData = { auto_renewing_30: { title: '30 天内自动续约', description: '30 天内将自动续约的生效合同' }, pending_approval: { title: '待审批', description: '审核中、金额超过审批阈值的合同' }, active_total_value: { title: '生效合同总金额', description: '所有生效合同金额之和' }, - expiring_table: { title: '即将到期的合同 (60 天内)', description: '按到期日升序排列的生效合同' }, + expiring_table: { + title: '即将到期的合同 (60 天内)', + description: '按到期日升序排列的生效合同', + }, pending_obligations: { title: '待办义务', description: '所有按到期日排序的待办义务' }, }, }, diff --git a/packages/contracts/src/views/contracts_contract.view.ts b/packages/contracts/src/views/contracts_contract.view.ts index 37573e0..2561732 100644 --- a/packages/contracts/src/views/contracts_contract.view.ts +++ b/packages/contracts/src/views/contracts_contract.view.ts @@ -35,7 +35,12 @@ export const ContractViews = defineView({ { name: 'all', label: 'All', view: 'all_contracts', isDefault: true, pinned: true }, { name: 'mine', label: 'My Contracts', icon: 'user', view: 'my_contracts' }, { name: 'expiring', label: 'Expiring ≤ 60d', icon: 'clock', view: 'expiring_contracts' }, - { name: 'pending', label: 'Pending Approval', icon: 'check', view: 'pending_approval_contracts' }, + { + name: 'pending', + label: 'Pending Approval', + icon: 'check', + view: 'pending_approval_contracts', + }, ], bulkActionDefs: [ { diff --git a/packages/expense/objectstack.manifest.json b/packages/expense/objectstack.manifest.json index 1447d48..e08fb8e 100644 --- a/packages/expense/objectstack.manifest.json +++ b/packages/expense/objectstack.manifest.json @@ -13,7 +13,12 @@ "iconUrl": "https://cdn.objectos.app/icons/expense.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/expense", "tags": ["expense", "reimbursement", "finance", "approvals"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md", "translations": { "zh-CN": { diff --git a/packages/expense/src/apps/expense.app.ts b/packages/expense/src/apps/expense.app.ts index 858afbb..712f443 100644 --- a/packages/expense/src/apps/expense.app.ts +++ b/packages/expense/src/apps/expense.app.ts @@ -17,8 +17,20 @@ export const ExpenseApp = App.create({ label: 'Overview', icon: 'gauge', }, - { id: 'nav_report', type: 'object', objectName: 'expense_report', label: 'Reports', icon: 'receipt' }, + { + id: 'nav_report', + type: 'object', + objectName: 'expense_report', + label: 'Reports', + icon: 'receipt', + }, { id: 'nav_line', type: 'object', objectName: 'expense_line', label: 'Lines', icon: 'list' }, - { id: 'nav_category', type: 'object', objectName: 'expense_category', label: 'Categories', icon: 'tag' }, + { + id: 'nav_category', + type: 'object', + objectName: 'expense_category', + label: 'Categories', + icon: 'tag', + }, ], }); diff --git a/packages/expense/src/dashboards/expenses_overview.dashboard.ts b/packages/expense/src/dashboards/expenses_overview.dashboard.ts index cbafd83..f5eb8b8 100644 --- a/packages/expense/src/dashboards/expenses_overview.dashboard.ts +++ b/packages/expense/src/dashboards/expenses_overview.dashboard.ts @@ -13,7 +13,8 @@ import type { Dashboard } from '@objectstack/spec/ui'; export const ExpensesOverviewDashboard: Dashboard = { name: 'expenses_overview_dashboard', label: 'Expenses Overview', - description: 'Reports awaiting approval, amounts owed to employees, and spend trend by month and category.', + description: + 'Reports awaiting approval, amounts owed to employees, and spend trend by month and category.', columns: 12, gap: 4, diff --git a/packages/expense/src/data/index.ts b/packages/expense/src/data/index.ts index 08379a3..75d2724 100644 --- a/packages/expense/src/data/index.ts +++ b/packages/expense/src/data/index.ts @@ -20,12 +20,36 @@ const categories = defineDataset(ExpenseCategory, { mode: 'upsert', externalId: 'code', records: [ - { name: 'Meals & Entertainment', code: 'MEALS', gl_account: '6300', per_txn_limit: 75, active: true }, + { + name: 'Meals & Entertainment', + code: 'MEALS', + gl_account: '6300', + per_txn_limit: 75, + active: true, + }, { name: 'Airfare', code: 'AIRFARE', gl_account: '6410', per_txn_limit: 1500, active: true }, { name: 'Lodging', code: 'LODGING', gl_account: '6420', per_txn_limit: 300, active: true }, - { name: 'Ground Transport', code: 'GROUND', gl_account: '6430', per_txn_limit: 100, active: true }, - { name: 'Office Supplies', code: 'SUPPLIES', gl_account: '6500', per_txn_limit: 200, active: true }, - { name: 'Software / SaaS', code: 'SOFTWARE', gl_account: '6600', per_txn_limit: 1000, active: true }, + { + name: 'Ground Transport', + code: 'GROUND', + gl_account: '6430', + per_txn_limit: 100, + active: true, + }, + { + name: 'Office Supplies', + code: 'SUPPLIES', + gl_account: '6500', + per_txn_limit: 200, + active: true, + }, + { + name: 'Software / SaaS', + code: 'SOFTWARE', + gl_account: '6600', + per_txn_limit: 1000, + active: true, + }, ], }); @@ -107,28 +131,154 @@ const lines = defineDataset(ExpenseLine, { externalId: 'description', records: [ // EXP-2026-0001 — Berlin offsite (2223) - { description: 'Round-trip flight SFO↔BER', expense_report: 'EXP-2026-0001', category: 'AIRFARE', expense_date: cel`daysAgo(40)`, merchant: 'Lufthansa', amount: 1240, payment_source: 'personal_card', receipt_attached: true }, - { description: 'Hotel — 3 nights, Mitte', expense_report: 'EXP-2026-0001', category: 'LODGING', expense_date: cel`daysAgo(37)`, merchant: 'Hotel Adlon', amount: 870, payment_source: 'personal_card', receipt_attached: true }, - { description: 'Team dinner — Berlin', expense_report: 'EXP-2026-0001', category: 'MEALS', expense_date: cel`daysAgo(36)`, merchant: 'Katz Orange', amount: 65, payment_source: 'personal_card', receipt_attached: false }, - { description: 'Airport taxi — Berlin', expense_report: 'EXP-2026-0001', category: 'GROUND', expense_date: cel`daysAgo(33)`, merchant: 'Taxi Berlin', amount: 48, payment_source: 'cash', receipt_attached: false }, + { + description: 'Round-trip flight SFO↔BER', + expense_report: 'EXP-2026-0001', + category: 'AIRFARE', + expense_date: cel`daysAgo(40)`, + merchant: 'Lufthansa', + amount: 1240, + payment_source: 'personal_card', + receipt_attached: true, + }, + { + description: 'Hotel — 3 nights, Mitte', + expense_report: 'EXP-2026-0001', + category: 'LODGING', + expense_date: cel`daysAgo(37)`, + merchant: 'Hotel Adlon', + amount: 870, + payment_source: 'personal_card', + receipt_attached: true, + }, + { + description: 'Team dinner — Berlin', + expense_report: 'EXP-2026-0001', + category: 'MEALS', + expense_date: cel`daysAgo(36)`, + merchant: 'Katz Orange', + amount: 65, + payment_source: 'personal_card', + receipt_attached: false, + }, + { + description: 'Airport taxi — Berlin', + expense_report: 'EXP-2026-0001', + category: 'GROUND', + expense_date: cel`daysAgo(33)`, + merchant: 'Taxi Berlin', + amount: 48, + payment_source: 'cash', + receipt_attached: false, + }, // EXP-2026-0002 — March client dinners (247) - { description: 'Dinner with Acme prospect', expense_report: 'EXP-2026-0002', category: 'MEALS', expense_date: cel`daysAgo(18)`, merchant: 'Quince', amount: 120, payment_source: 'personal_card', receipt_attached: true }, - { description: 'Lunch with Globex lead', expense_report: 'EXP-2026-0002', category: 'MEALS', expense_date: cel`daysAgo(12)`, merchant: 'Tartine', amount: 95, payment_source: 'personal_card', receipt_attached: true }, - { description: 'Rideshare to client office', expense_report: 'EXP-2026-0002', category: 'GROUND', expense_date: cel`daysAgo(12)`, merchant: 'Uber', amount: 32, payment_source: 'personal_card', receipt_attached: false }, + { + description: 'Dinner with Acme prospect', + expense_report: 'EXP-2026-0002', + category: 'MEALS', + expense_date: cel`daysAgo(18)`, + merchant: 'Quince', + amount: 120, + payment_source: 'personal_card', + receipt_attached: true, + }, + { + description: 'Lunch with Globex lead', + expense_report: 'EXP-2026-0002', + category: 'MEALS', + expense_date: cel`daysAgo(12)`, + merchant: 'Tartine', + amount: 95, + payment_source: 'personal_card', + receipt_attached: true, + }, + { + description: 'Rideshare to client office', + expense_report: 'EXP-2026-0002', + category: 'GROUND', + expense_date: cel`daysAgo(12)`, + merchant: 'Uber', + amount: 32, + payment_source: 'personal_card', + receipt_attached: false, + }, // EXP-2026-0003 — Home office setup (550) - { description: 'Standing desk', expense_report: 'EXP-2026-0003', category: 'SUPPLIES', expense_date: cel`daysAgo(26)`, merchant: 'Fully', amount: 240, payment_source: 'personal_card', receipt_attached: true }, - { description: 'Chair mat & cable tray', expense_report: 'EXP-2026-0003', category: 'SUPPLIES', expense_date: cel`daysAgo(24)`, merchant: 'Amazon', amount: 130, payment_source: 'personal_card', receipt_attached: true }, - { description: 'Figma annual seat', expense_report: 'EXP-2026-0003', category: 'SOFTWARE', expense_date: cel`daysAgo(22)`, merchant: 'Figma', amount: 180, payment_source: 'personal_card', receipt_attached: true }, + { + description: 'Standing desk', + expense_report: 'EXP-2026-0003', + category: 'SUPPLIES', + expense_date: cel`daysAgo(26)`, + merchant: 'Fully', + amount: 240, + payment_source: 'personal_card', + receipt_attached: true, + }, + { + description: 'Chair mat & cable tray', + expense_report: 'EXP-2026-0003', + category: 'SUPPLIES', + expense_date: cel`daysAgo(24)`, + merchant: 'Amazon', + amount: 130, + payment_source: 'personal_card', + receipt_attached: true, + }, + { + description: 'Figma annual seat', + expense_report: 'EXP-2026-0003', + category: 'SOFTWARE', + expense_date: cel`daysAgo(22)`, + merchant: 'Figma', + amount: 180, + payment_source: 'personal_card', + receipt_attached: true, + }, // EXP-2026-0004 — SaaStr (850, draft) - { description: 'Flight SFO↔SJC (SaaStr)', expense_report: 'EXP-2026-0004', category: 'AIRFARE', expense_date: cel`daysFromNow(10)`, merchant: 'United', amount: 560, payment_source: 'personal_card', receipt_attached: true }, - { description: 'Hotel — 1 night (SaaStr)', expense_report: 'EXP-2026-0004', category: 'LODGING', expense_date: cel`daysFromNow(10)`, merchant: 'Signia', amount: 290, payment_source: 'personal_card', receipt_attached: true }, + { + description: 'Flight SFO↔SJC (SaaStr)', + expense_report: 'EXP-2026-0004', + category: 'AIRFARE', + expense_date: cel`daysFromNow(10)`, + merchant: 'United', + amount: 560, + payment_source: 'personal_card', + receipt_attached: true, + }, + { + description: 'Hotel — 1 night (SaaStr)', + expense_report: 'EXP-2026-0004', + category: 'LODGING', + expense_date: cel`daysFromNow(10)`, + merchant: 'Signia', + amount: 290, + payment_source: 'personal_card', + receipt_attached: true, + }, // EXP-2026-0005 — Misc taxis (69, rejected) - { description: 'Taxi — downtown', expense_report: 'EXP-2026-0005', category: 'GROUND', expense_date: cel`daysAgo(17)`, merchant: 'Flywheel', amount: 28, payment_source: 'cash', receipt_attached: false }, - { description: 'Taxi — airport return', expense_report: 'EXP-2026-0005', category: 'GROUND', expense_date: cel`daysAgo(13)`, merchant: 'Flywheel', amount: 41, payment_source: 'cash', receipt_attached: false }, + { + description: 'Taxi — downtown', + expense_report: 'EXP-2026-0005', + category: 'GROUND', + expense_date: cel`daysAgo(17)`, + merchant: 'Flywheel', + amount: 28, + payment_source: 'cash', + receipt_attached: false, + }, + { + description: 'Taxi — airport return', + expense_report: 'EXP-2026-0005', + category: 'GROUND', + expense_date: cel`daysAgo(13)`, + merchant: 'Flywheel', + amount: 41, + payment_source: 'cash', + receipt_attached: false, + }, ], }); diff --git a/packages/expense/src/objects/expense_category.object.ts b/packages/expense/src/objects/expense_category.object.ts index 6b4d90b..03695ad 100644 --- a/packages/expense/src/objects/expense_category.object.ts +++ b/packages/expense/src/objects/expense_category.object.ts @@ -16,8 +16,7 @@ export const ExpenseCategory = ObjectSchema.create({ label: 'Expense Category', pluralLabel: 'Expense Categories', icon: 'tag', - description: - 'A spend type (meals, travel, lodging, …) used to code and report expense lines.', + description: 'A spend type (meals, travel, lodging, …) used to code and report expense lines.', fields: { name: Field.text({ @@ -40,8 +39,7 @@ export const ExpenseCategory = ObjectSchema.create({ per_txn_limit: Field.currency({ label: 'Per-Transaction Limit', min: 0, - description: - 'Soft cap shown to submitters. Hard enforcement is a fork point.', + description: 'Soft cap shown to submitters. Hard enforcement is a fork point.', }), active: Field.boolean({ label: 'Active', diff --git a/packages/expense/src/objects/expense_line.object.ts b/packages/expense/src/objects/expense_line.object.ts index 364a494..a1e0d85 100644 --- a/packages/expense/src/objects/expense_line.object.ts +++ b/packages/expense/src/objects/expense_line.object.ts @@ -90,11 +90,7 @@ export const ExpenseLine = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['expense_report'] }, - { fields: ['category'] }, - { fields: ['expense_date'] }, - ], + indexes: [{ fields: ['expense_report'] }, { fields: ['category'] }, { fields: ['expense_date'] }], titleFormat: tmpl`{{record.description}}`, displayNameField: 'description', diff --git a/packages/expense/src/objects/expense_report.object.ts b/packages/expense/src/objects/expense_report.object.ts index a245b62..2b8c43f 100644 --- a/packages/expense/src/objects/expense_report.object.ts +++ b/packages/expense/src/objects/expense_report.object.ts @@ -145,7 +145,13 @@ export const ExpenseReport = ObjectSchema.create({ type: 'state_machine', name: 'expense_report_lifecycle', field: 'status', - transitions: {draft:["submitted"], submitted:["approved", "rejected", "draft"], approved:["reimbursed"], rejected:["draft"], reimbursed:[]}, + transitions: { + draft: ['submitted'], + submitted: ['approved', 'rejected', 'draft'], + approved: ['reimbursed'], + rejected: ['draft'], + reimbursed: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/expense/src/sharing/index.ts b/packages/expense/src/sharing/index.ts index 60c0fd9..2e6e801 100644 --- a/packages/expense/src/sharing/index.ts +++ b/packages/expense/src/sharing/index.ts @@ -11,7 +11,11 @@ export const RoleHierarchy = { roles: [ { name: 'expense_admin', label: 'Expense Admin', parentRole: null as string | null }, - { name: 'expense_manager', label: 'Expense Manager', parentRole: 'expense_admin' as string | null }, + { + name: 'expense_manager', + label: 'Expense Manager', + parentRole: 'expense_admin' as string | null, + }, { name: 'expense_employee', label: 'Employee', parentRole: 'expense_manager' as string | null }, ], }; diff --git a/packages/expense/src/translations/en.ts b/packages/expense/src/translations/en.ts index 4762b3b..2000d61 100644 --- a/packages/expense/src/translations/en.ts +++ b/packages/expense/src/translations/en.ts @@ -57,7 +57,12 @@ export const en: TranslationData = { reimbursed_at: { label: 'Reimbursed At' }, payment_method: { label: 'Payment Method', - options: { bank_transfer: 'Bank Transfer', payroll: 'Payroll', cash: 'Cash', check: 'Check' }, + options: { + bank_transfer: 'Bank Transfer', + payroll: 'Payroll', + cash: 'Cash', + check: 'Check', + }, }, payment_reference: { label: 'Payment Reference' }, notes: { label: 'Internal Notes' }, @@ -66,8 +71,14 @@ export const en: TranslationData = { all_reports: { label: 'All Reports', description: 'Every report, grouped by status' }, report_pipeline: { label: 'Report Pipeline', description: 'Kanban grouped by status' }, my_reports: { label: 'My Reports', description: 'Reports where you are the employee' }, - awaiting_approval: { label: 'Awaiting Approval', description: 'Submitted reports pending a decision' }, - awaiting_reimbursement: { label: 'To Reimburse', description: 'Approved reports awaiting payment' }, + awaiting_approval: { + label: 'Awaiting Approval', + description: 'Submitted reports pending a decision', + }, + awaiting_reimbursement: { + label: 'To Reimburse', + description: 'Approved reports awaiting payment', + }, }, }, @@ -84,7 +95,11 @@ export const en: TranslationData = { amount: { label: 'Amount' }, payment_source: { label: 'Paid With', - options: { personal_card: 'Personal Card', cash: 'Cash', personal_other: 'Personal — Other' }, + options: { + personal_card: 'Personal Card', + cash: 'Cash', + personal_other: 'Personal — Other', + }, }, needs_receipt: { label: 'Receipt Required' }, receipt_attached: { label: 'Receipt Attached' }, @@ -137,14 +152,35 @@ export const en: TranslationData = { description: 'Reports awaiting approval, amounts owed, and spend trend.', actions: { create_report: { label: 'New Report' } }, widgets: { - awaiting_approval: { title: 'Awaiting Approval', description: 'Submitted reports pending a decision' }, - awaiting_reimbursement: { title: 'To Reimburse', description: 'Approved reports awaiting payment' }, - owed_amount: { title: 'Owed to Employees ($)', description: 'Total of approved, unpaid reports' }, + awaiting_approval: { + title: 'Awaiting Approval', + description: 'Submitted reports pending a decision', + }, + awaiting_reimbursement: { + title: 'To Reimburse', + description: 'Approved reports awaiting payment', + }, + owed_amount: { + title: 'Owed to Employees ($)', + description: 'Total of approved, unpaid reports', + }, reimbursed_total: { title: 'Reimbursed ($)', description: 'Total reimbursed to date' }, - pending_reports_table: { title: 'Reports Awaiting Approval', description: 'Submitted reports sorted by amount' }, - to_reimburse_table: { title: 'Approved — To Reimburse', description: 'Approved reports awaiting payment' }, - spend_by_category: { title: 'Spend by Category', description: 'Line amounts grouped by category' }, - spend_by_month: { title: 'Reimbursed by Month', description: 'Reimbursed totals over the last 12 months' }, + pending_reports_table: { + title: 'Reports Awaiting Approval', + description: 'Submitted reports sorted by amount', + }, + to_reimburse_table: { + title: 'Approved — To Reimburse', + description: 'Approved reports awaiting payment', + }, + spend_by_category: { + title: 'Spend by Category', + description: 'Line amounts grouped by category', + }, + spend_by_month: { + title: 'Reimbursed by Month', + description: 'Reimbursed totals over the last 12 months', + }, }, }, }, diff --git a/packages/helpdesk/objectstack.manifest.json b/packages/helpdesk/objectstack.manifest.json index 2d8376e..e1ba1a3 100644 --- a/packages/helpdesk/objectstack.manifest.json +++ b/packages/helpdesk/objectstack.manifest.json @@ -12,7 +12,12 @@ "iconUrl": "https://cdn.objectos.app/icons/helpdesk.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/helpdesk", "tags": ["support", "ai", "tickets", "kb"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md", "translations": { "zh-CN": { diff --git a/packages/helpdesk/src/apps/helpdesk.app.ts b/packages/helpdesk/src/apps/helpdesk.app.ts index be14d24..3cab4f3 100644 --- a/packages/helpdesk/src/apps/helpdesk.app.ts +++ b/packages/helpdesk/src/apps/helpdesk.app.ts @@ -24,11 +24,41 @@ export const HelpdeskApp = App.create({ label: 'Manager Overview', icon: 'bar-chart-3', }, - { id: 'nav_tickets', type: 'object', objectName: 'helpdesk_ticket', label: 'Tickets', icon: 'life-buoy' }, - { id: 'nav_messages', type: 'object', objectName: 'helpdesk_message', label: 'Messages', icon: 'message-circle' }, - { id: 'nav_customers', type: 'object', objectName: 'helpdesk_customer', label: 'Customers', icon: 'user' }, - { id: 'nav_kb', type: 'object', objectName: 'helpdesk_kb_article', label: 'Knowledge Base', icon: 'book-open' }, + { + id: 'nav_tickets', + type: 'object', + objectName: 'helpdesk_ticket', + label: 'Tickets', + icon: 'life-buoy', + }, + { + id: 'nav_messages', + type: 'object', + objectName: 'helpdesk_message', + label: 'Messages', + icon: 'message-circle', + }, + { + id: 'nav_customers', + type: 'object', + objectName: 'helpdesk_customer', + label: 'Customers', + icon: 'user', + }, + { + id: 'nav_kb', + type: 'object', + objectName: 'helpdesk_kb_article', + label: 'Knowledge Base', + icon: 'book-open', + }, { id: 'nav_teams', type: 'object', objectName: 'helpdesk_team', label: 'Teams', icon: 'users' }, - { id: 'nav_sla', type: 'object', objectName: 'helpdesk_sla_policy', label: 'SLA Policies', icon: 'clock' }, + { + id: 'nav_sla', + type: 'object', + objectName: 'helpdesk_sla_policy', + label: 'SLA Policies', + icon: 'clock', + }, ], }); diff --git a/packages/helpdesk/src/dashboards/manager_overview.dashboard.ts b/packages/helpdesk/src/dashboards/manager_overview.dashboard.ts index 7eeb6d3..3225309 100644 --- a/packages/helpdesk/src/dashboards/manager_overview.dashboard.ts +++ b/packages/helpdesk/src/dashboards/manager_overview.dashboard.ts @@ -125,7 +125,15 @@ export const ManagerOverviewDashboard: Dashboard = { filter: { status: 'escalated' }, layout: { x: 0, y: 10, w: 12, h: 5 }, options: { - columns: ['ticket_number', 'name', 'customer', 'team', 'assignee', 'ai_sentiment', 'priority'], + columns: [ + 'ticket_number', + 'name', + 'customer', + 'team', + 'assignee', + 'ai_sentiment', + 'priority', + ], pageSize: 10, sort: [{ field: 'priority', order: 'desc' }], }, diff --git a/packages/helpdesk/src/data/index.ts b/packages/helpdesk/src/data/index.ts index 39df083..b33e945 100644 --- a/packages/helpdesk/src/data/index.ts +++ b/packages/helpdesk/src/data/index.ts @@ -24,9 +24,27 @@ const teams = defineDataset(Team, { mode: 'upsert', externalId: 'code', records: [ - { name: 'Tier 1 Support', code: 'T1', specialty: 'tier1', is_active: true, business_hours: 'Mon–Fri 09:00–18:00 Asia/Shanghai' }, - { name: 'Tier 2 Engineering', code: 'T2', specialty: 'tier2', is_active: true, business_hours: 'Mon–Fri 10:00–19:00 UTC' }, - { name: 'Billing Team', code: 'BILL', specialty: 'billing', is_active: true, business_hours: 'Mon–Fri 09:00–17:00 Asia/Shanghai' }, + { + name: 'Tier 1 Support', + code: 'T1', + specialty: 'tier1', + is_active: true, + business_hours: 'Mon–Fri 09:00–18:00 Asia/Shanghai', + }, + { + name: 'Tier 2 Engineering', + code: 'T2', + specialty: 'tier2', + is_active: true, + business_hours: 'Mon–Fri 10:00–19:00 UTC', + }, + { + name: 'Billing Team', + code: 'BILL', + specialty: 'billing', + is_active: true, + business_hours: 'Mon–Fri 09:00–17:00 Asia/Shanghai', + }, ], }); @@ -38,38 +56,54 @@ const slaPolicies = defineDataset(SLAPolicy, { name: 'Free Tier SLA', applies_to_tier: 'free', is_default: true, - first_response_low_minutes: 2880, first_response_normal_minutes: 1440, - first_response_high_minutes: 480, first_response_urgent_minutes: 240, - resolution_low_minutes: 20160, resolution_normal_minutes: 10080, - resolution_high_minutes: 4320, resolution_urgent_minutes: 1440, + first_response_low_minutes: 2880, + first_response_normal_minutes: 1440, + first_response_high_minutes: 480, + first_response_urgent_minutes: 240, + resolution_low_minutes: 20160, + resolution_normal_minutes: 10080, + resolution_high_minutes: 4320, + resolution_urgent_minutes: 1440, notes: 'Best-effort response. Community-supported.', }, { name: 'Pro Tier SLA', applies_to_tier: 'pro', is_default: false, - first_response_low_minutes: 1440, first_response_normal_minutes: 480, - first_response_high_minutes: 120, first_response_urgent_minutes: 60, - resolution_low_minutes: 10080, resolution_normal_minutes: 2880, - resolution_high_minutes: 1440, resolution_urgent_minutes: 240, + first_response_low_minutes: 1440, + first_response_normal_minutes: 480, + first_response_high_minutes: 120, + first_response_urgent_minutes: 60, + resolution_low_minutes: 10080, + resolution_normal_minutes: 2880, + resolution_high_minutes: 1440, + resolution_urgent_minutes: 240, }, { name: 'Business Tier SLA', applies_to_tier: 'business', is_default: false, - first_response_low_minutes: 480, first_response_normal_minutes: 240, - first_response_high_minutes: 60, first_response_urgent_minutes: 30, - resolution_low_minutes: 4320, resolution_normal_minutes: 1440, - resolution_high_minutes: 480, resolution_urgent_minutes: 120, + first_response_low_minutes: 480, + first_response_normal_minutes: 240, + first_response_high_minutes: 60, + first_response_urgent_minutes: 30, + resolution_low_minutes: 4320, + resolution_normal_minutes: 1440, + resolution_high_minutes: 480, + resolution_urgent_minutes: 120, }, { name: 'Enterprise Tier SLA', applies_to_tier: 'enterprise', is_default: false, - first_response_low_minutes: 240, first_response_normal_minutes: 60, - first_response_high_minutes: 30, first_response_urgent_minutes: 15, - resolution_low_minutes: 1440, resolution_normal_minutes: 480, - resolution_high_minutes: 240, resolution_urgent_minutes: 60, + first_response_low_minutes: 240, + first_response_normal_minutes: 60, + first_response_high_minutes: 30, + first_response_urgent_minutes: 15, + resolution_low_minutes: 1440, + resolution_normal_minutes: 480, + resolution_high_minutes: 240, + resolution_urgent_minutes: 60, notes: '24×7 coverage. Dedicated TAM.', }, ], @@ -79,12 +113,54 @@ const customers = defineDataset(Customer, { mode: 'upsert', externalId: 'email', records: [ - { name: 'Alice Chen', email: 'alice.chen@acme.example.com', company: 'Acme Inc.', tier: 'enterprise', locale: 'en', timezone: 'America/Los_Angeles' }, - { name: '李伟', email: 'li.wei@globex.example.com', company: 'Globex 科技', tier: 'business', locale: 'zh-CN', timezone: 'Asia/Shanghai' }, - { name: 'Bob Tanaka', email: 'bob.tanaka@hooli.example.com', company: 'Hooli LLC', tier: 'pro', locale: 'en', timezone: 'Asia/Tokyo' }, - { name: 'Sofia García', email: 'sofia.garcia@initech.example.com', company: 'Initech', tier: 'pro', locale: 'en', timezone: 'Europe/Madrid' }, - { name: 'Daniel Kim', email: 'daniel.kim@stark.example.com', company: 'Stark Industries', tier: 'free', locale: 'en', timezone: 'America/New_York' }, - { name: '王芳', email: 'wang.fang@umbrella.example.com', company: 'Umbrella 集团', tier: 'free', locale: 'zh-CN', timezone: 'Asia/Shanghai' }, + { + name: 'Alice Chen', + email: 'alice.chen@acme.example.com', + company: 'Acme Inc.', + tier: 'enterprise', + locale: 'en', + timezone: 'America/Los_Angeles', + }, + { + name: '李伟', + email: 'li.wei@globex.example.com', + company: 'Globex 科技', + tier: 'business', + locale: 'zh-CN', + timezone: 'Asia/Shanghai', + }, + { + name: 'Bob Tanaka', + email: 'bob.tanaka@hooli.example.com', + company: 'Hooli LLC', + tier: 'pro', + locale: 'en', + timezone: 'Asia/Tokyo', + }, + { + name: 'Sofia García', + email: 'sofia.garcia@initech.example.com', + company: 'Initech', + tier: 'pro', + locale: 'en', + timezone: 'Europe/Madrid', + }, + { + name: 'Daniel Kim', + email: 'daniel.kim@stark.example.com', + company: 'Stark Industries', + tier: 'free', + locale: 'en', + timezone: 'America/New_York', + }, + { + name: '王芳', + email: 'wang.fang@umbrella.example.com', + company: 'Umbrella 集团', + tier: 'free', + locale: 'zh-CN', + timezone: 'Asia/Shanghai', + }, ], }); @@ -96,52 +172,69 @@ const kbArticles = defineDataset(KBArticle, { name: 'How to reset your password', slug: 'reset-password', body: '1. Open the login page.\n2. Click "Forgot password".\n3. Check your inbox for the reset link...', - category: 'how_to', status: 'published', locale: 'en', + category: 'how_to', + status: 'published', + locale: 'en', tags: ['password', 'login', 'reset'], - helpful_count: 142, unhelpful_count: 3, + helpful_count: 142, + unhelpful_count: 3, published_at: cel`daysAgo(90)`, }, { name: '如何重置密码', slug: 'reset-password-zh', body: '1. 打开登录页面。\n2. 点击"忘记密码"。\n3. 查收邮箱中的重置链接...', - category: 'how_to', status: 'published', locale: 'zh-CN', + category: 'how_to', + status: 'published', + locale: 'zh-CN', tags: ['密码', '登录', '重置'], - helpful_count: 88, unhelpful_count: 1, + helpful_count: 88, + unhelpful_count: 1, published_at: cel`daysAgo(60)`, }, { name: 'Understanding your invoice', slug: 'understanding-invoice', body: 'Your monthly invoice contains the following sections...', - category: 'billing', status: 'published', locale: 'en', + category: 'billing', + status: 'published', + locale: 'en', tags: ['invoice', 'billing', 'charges'], - helpful_count: 67, unhelpful_count: 8, + helpful_count: 67, + unhelpful_count: 8, published_at: cel`daysAgo(45)`, }, { name: 'API rate limit reached — what to do', slug: 'api-rate-limit', body: 'If you see HTTP 429, you have exceeded your tier rate limit...', - category: 'api', status: 'published', locale: 'en', + category: 'api', + status: 'published', + locale: 'en', tags: ['api', 'rate-limit', '429'], - helpful_count: 54, unhelpful_count: 2, + helpful_count: 54, + unhelpful_count: 2, published_at: cel`daysAgo(30)`, }, { name: 'Known issue: Safari export bug', slug: 'safari-export-bug', body: 'Affected versions: Safari 17.0–17.2. Workaround: use Chrome or Firefox.', - category: 'known_issues', status: 'published', locale: 'en', + category: 'known_issues', + status: 'published', + locale: 'en', tags: ['safari', 'export', 'bug'], - helpful_count: 12, unhelpful_count: 0, + helpful_count: 12, + unhelpful_count: 0, published_at: cel`daysAgo(7)`, }, { name: '[Draft] Setting up SSO with Okta', slug: 'sso-okta-setup', body: 'Work in progress.', - category: 'getting_started', status: 'draft', locale: 'en', + category: 'getting_started', + status: 'draft', + locale: 'en', tags: ['sso', 'okta'], }, ], @@ -154,21 +247,24 @@ const tickets = defineDataset(Ticket, { { ticket_number: 'TIC-2026-001', name: 'Cannot log in after password reset', - description: 'I reset my password via the email link 30 minutes ago. The page says "success" but I still get "invalid credentials" on login. Please help.', + description: + 'I reset my password via the email link 30 minutes ago. The page says "success" but I still get "invalid credentials" on login. Please help.', channel: 'email', status: 'new', priority: 'high', customer: 'alice.chen@acme.example.com', team: 'T1', sla_policy: 'Enterprise Tier SLA', - ai_summary: 'Customer cannot log in despite successful password reset. Likely cache or session sync issue.', + ai_summary: + 'Customer cannot log in despite successful password reset. Likely cache or session sync issue.', ai_category: 'bug', ai_intent: 'Restore login access', ai_sentiment: 'frustrated', ai_priority_suggestion: 'high', ai_language: 'en', ai_confidence: 0.86, - ai_suggested_reply: 'Hi Alice,\n\nThanks for reaching out — sorry you\'re hitting this. A few quick checks:\n1. Could you try a hard refresh (Cmd+Shift+R) or an incognito window?\n2. If you still see "invalid credentials", reply to this email and I\'ll trigger a session reset on our side.\n\nBest,\nSupport', + ai_suggested_reply: + 'Hi Alice,\n\nThanks for reaching out — sorry you\'re hitting this. A few quick checks:\n1. Could you try a hard refresh (Cmd+Shift+R) or an incognito window?\n2. If you still see "invalid credentials", reply to this email and I\'ll trigger a session reset on our side.\n\nBest,\nSupport', ai_suggested_kb_ids: ['reset-password'], ai_triage_at: cel`daysAgo(0)`, first_response_due_at: cel`daysFromNow(0)`, @@ -191,7 +287,8 @@ const tickets = defineDataset(Ticket, { ai_priority_suggestion: 'normal', ai_language: 'zh-CN', ai_confidence: 0.78, - ai_suggested_reply: '李先生,您好:\n\n感谢您的来信。我们已经在核对您本月的用量明细,预计 4 小时内会有详细回复。\n\n初步排查方向:\n1. 套餐内 vs 套餐外用量\n2. 是否有新启用的高级功能\n\n如有疑问随时联系。\n\n客户服务', + ai_suggested_reply: + '李先生,您好:\n\n感谢您的来信。我们已经在核对您本月的用量明细,预计 4 小时内会有详细回复。\n\n初步排查方向:\n1. 套餐内 vs 套餐外用量\n2. 是否有新启用的高级功能\n\n如有疑问随时联系。\n\n客户服务', ai_suggested_kb_ids: ['understanding-invoice'], ai_triage_at: cel`daysAgo(0)`, first_response_due_at: cel`daysFromNow(0)`, @@ -200,21 +297,24 @@ const tickets = defineDataset(Ticket, { { ticket_number: 'TIC-2026-003', name: 'API returns 429 for every request', - description: 'Since this morning every call returns HTTP 429. We are on Pro tier and well below documented limits. This is killing our production traffic — fix immediately.', + description: + 'Since this morning every call returns HTTP 429. We are on Pro tier and well below documented limits. This is killing our production traffic — fix immediately.', channel: 'api', status: 'in_progress', priority: 'urgent', customer: 'bob.tanaka@hooli.example.com', team: 'T2', sla_policy: 'Pro Tier SLA', - ai_summary: 'Pro-tier customer hitting unexpected HTTP 429 on all API calls since this morning; production impact.', + ai_summary: + 'Pro-tier customer hitting unexpected HTTP 429 on all API calls since this morning; production impact.', ai_category: 'outage', ai_intent: 'Restore API access immediately', ai_sentiment: 'angry', ai_priority_suggestion: 'urgent', ai_language: 'en', ai_confidence: 0.92, - ai_suggested_reply: 'Hi Bob,\n\nWe see the impact and have engineering looking now. Confirmed your account is below the documented rate limit. Investigating an upstream limiter misconfiguration. Will update within 15 minutes.\n\nIncident channel: #inc-2026-001\n\n— On-call SRE', + ai_suggested_reply: + 'Hi Bob,\n\nWe see the impact and have engineering looking now. Confirmed your account is below the documented rate limit. Investigating an upstream limiter misconfiguration. Will update within 15 minutes.\n\nIncident channel: #inc-2026-001\n\n— On-call SRE', ai_suggested_kb_ids: ['api-rate-limit'], ai_triage_at: cel`daysAgo(0)`, first_response_due_at: cel`daysAgo(0)`, @@ -224,7 +324,8 @@ const tickets = defineDataset(Ticket, { { ticket_number: 'TIC-2026-004', name: 'Feature request: bulk export to Parquet', - description: 'CSV export is fine but for our 50M row dataset we really need Parquet. Any chance you could add this?', + description: + 'CSV export is fine but for our 50M row dataset we really need Parquet. Any chance you could add this?', channel: 'web', status: 'in_progress', priority: 'low', @@ -238,7 +339,8 @@ const tickets = defineDataset(Ticket, { ai_priority_suggestion: 'low', ai_language: 'en', ai_confidence: 0.81, - ai_suggested_reply: 'Hi Sofia,\n\nGreat suggestion — Parquet support is on our radar. Logged this as a +1 to the backlog item. Would you also want column projection at export time?\n\n— Support', + ai_suggested_reply: + 'Hi Sofia,\n\nGreat suggestion — Parquet support is on our radar. Logged this as a +1 to the backlog item. Would you also want column projection at export time?\n\n— Support', ai_suggested_kb_ids: [], ai_triage_at: cel`daysAgo(1)`, first_response_due_at: cel`daysAgo(0)`, @@ -248,21 +350,24 @@ const tickets = defineDataset(Ticket, { { ticket_number: 'TIC-2026-005', name: 'Safari export downloads empty CSV', - description: 'Hi, exporting any list view in Safari 17.2 gives me a 0-byte CSV. Chrome works fine. Not urgent for me but maybe known issue?', + description: + 'Hi, exporting any list view in Safari 17.2 gives me a 0-byte CSV. Chrome works fine. Not urgent for me but maybe known issue?', channel: 'web', status: 'waiting_customer', priority: 'normal', customer: 'daniel.kim@stark.example.com', team: 'T1', sla_policy: 'Free Tier SLA', - ai_summary: 'Customer reports empty CSV export on Safari 17.2; works in Chrome. Matches known issue.', + ai_summary: + 'Customer reports empty CSV export on Safari 17.2; works in Chrome. Matches known issue.', ai_category: 'bug', ai_intent: 'Confirm known issue + workaround', ai_sentiment: 'neutral', ai_priority_suggestion: 'normal', ai_language: 'en', ai_confidence: 0.95, - ai_suggested_reply: 'Hi Daniel,\n\nYou\'ve hit a known Safari issue — see the KB link below. Workaround is to use Chrome or Firefox until our 17.x fix ships next week. Letting you know we\'re tracking it.\n\n— Support', + ai_suggested_reply: + "Hi Daniel,\n\nYou've hit a known Safari issue — see the KB link below. Workaround is to use Chrome or Firefox until our 17.x fix ships next week. Letting you know we're tracking it.\n\n— Support", ai_suggested_kb_ids: ['safari-export-bug'], ai_triage_at: cel`daysAgo(2)`, first_response_due_at: cel`daysAgo(1)`, @@ -272,7 +377,8 @@ const tickets = defineDataset(Ticket, { { ticket_number: 'TIC-2026-006', name: '系统经常崩溃,已经第三次反馈了!', - description: '从上周开始系统每天崩溃 2-3 次。我已经反馈了三次没有任何反馈。你们是不是不打算解决了?现在严重影响我们工作!', + description: + '从上周开始系统每天崩溃 2-3 次。我已经反馈了三次没有任何反馈。你们是不是不打算解决了?现在严重影响我们工作!', channel: 'phone', status: 'escalated', priority: 'urgent', @@ -286,7 +392,8 @@ const tickets = defineDataset(Ticket, { ai_priority_suggestion: 'urgent', ai_language: 'zh-CN', ai_confidence: 0.97, - ai_suggested_reply: '王女士,您好:\n\n非常抱歉之前的沟通让您失望。这是不应该发生的。我已经接手您的案例,请允许我 1 小时内同您电话沟通详细情况,并把所有崩溃日志带到技术团队优先处理。\n\n直接联系:support-lead@example.com\n\n支持主管', + ai_suggested_reply: + '王女士,您好:\n\n非常抱歉之前的沟通让您失望。这是不应该发生的。我已经接手您的案例,请允许我 1 小时内同您电话沟通详细情况,并把所有崩溃日志带到技术团队优先处理。\n\n直接联系:support-lead@example.com\n\n支持主管', ai_suggested_kb_ids: [], ai_triage_at: cel`daysAgo(1)`, first_response_due_at: cel`daysAgo(2)`, @@ -295,21 +402,24 @@ const tickets = defineDataset(Ticket, { { ticket_number: 'TIC-2026-007', name: 'Onboarding walkthrough was excellent', - description: 'Just wanted to say the onboarding video and the inline tips really helped. 10/10. No issue, just kudos.', + description: + 'Just wanted to say the onboarding video and the inline tips really helped. 10/10. No issue, just kudos.', channel: 'email', status: 'resolved', priority: 'low', customer: 'sofia.garcia@initech.example.com', team: 'T1', sla_policy: 'Pro Tier SLA', - ai_summary: 'Customer praising onboarding experience — no issue to resolve, send appreciation.', + ai_summary: + 'Customer praising onboarding experience — no issue to resolve, send appreciation.', ai_category: 'feedback', ai_intent: 'Share positive feedback', ai_sentiment: 'positive', ai_priority_suggestion: 'low', ai_language: 'en', ai_confidence: 0.99, - ai_suggested_reply: 'Thanks so much, Sofia! Forwarding this to the product team — made our day.\n\n— Support', + ai_suggested_reply: + 'Thanks so much, Sofia! Forwarding this to the product team — made our day.\n\n— Support', ai_suggested_kb_ids: [], ai_triage_at: cel`daysAgo(2)`, first_response_due_at: cel`daysAgo(1)`, @@ -336,7 +446,8 @@ const tickets = defineDataset(Ticket, { ai_priority_suggestion: 'low', ai_language: 'en', ai_confidence: 0.94, - ai_suggested_reply: 'Hi Daniel — head to Settings → Team → Invite member. Enter their email and pick a role. They\'ll get an invite link by email.\n\n— Support', + ai_suggested_reply: + "Hi Daniel — head to Settings → Team → Invite member. Enter their email and pick a role. They'll get an invite link by email.\n\n— Support", ai_suggested_kb_ids: [], ai_triage_at: cel`daysAgo(3)`, first_response_due_at: cel`daysAgo(2)`, diff --git a/packages/helpdesk/src/objects/helpdesk_kb_article.object.ts b/packages/helpdesk/src/objects/helpdesk_kb_article.object.ts index 8616007..9277c66 100644 --- a/packages/helpdesk/src/objects/helpdesk_kb_article.object.ts +++ b/packages/helpdesk/src/objects/helpdesk_kb_article.object.ts @@ -85,7 +85,12 @@ export const KBArticle = ObjectSchema.create({ type: 'state_machine', name: 'kb_article_lifecycle', field: 'status', - transitions: {draft:["review"], review:["published", "draft"], published:["archived", "draft"], archived:["draft"]}, + transitions: { + draft: ['review'], + review: ['published', 'draft'], + published: ['archived', 'draft'], + archived: ['draft'], + }, message: 'Illegal status transition.', }, ], diff --git a/packages/helpdesk/src/objects/helpdesk_sla_policy.object.ts b/packages/helpdesk/src/objects/helpdesk_sla_policy.object.ts index b745afd..d9c06be 100644 --- a/packages/helpdesk/src/objects/helpdesk_sla_policy.object.ts +++ b/packages/helpdesk/src/objects/helpdesk_sla_policy.object.ts @@ -41,15 +41,33 @@ export const SLAPolicy = ObjectSchema.create({ description: 'Fallback policy when no tier match.', }), - first_response_low_minutes: Field.number({ label: 'First Response — Low (min)', defaultValue: 1440 }), - first_response_normal_minutes: Field.number({ label: 'First Response — Normal (min)', defaultValue: 480 }), - first_response_high_minutes: Field.number({ label: 'First Response — High (min)', defaultValue: 120 }), - first_response_urgent_minutes: Field.number({ label: 'First Response — Urgent (min)', defaultValue: 30 }), + first_response_low_minutes: Field.number({ + label: 'First Response — Low (min)', + defaultValue: 1440, + }), + first_response_normal_minutes: Field.number({ + label: 'First Response — Normal (min)', + defaultValue: 480, + }), + first_response_high_minutes: Field.number({ + label: 'First Response — High (min)', + defaultValue: 120, + }), + first_response_urgent_minutes: Field.number({ + label: 'First Response — Urgent (min)', + defaultValue: 30, + }), resolution_low_minutes: Field.number({ label: 'Resolution — Low (min)', defaultValue: 10080 }), - resolution_normal_minutes: Field.number({ label: 'Resolution — Normal (min)', defaultValue: 2880 }), + resolution_normal_minutes: Field.number({ + label: 'Resolution — Normal (min)', + defaultValue: 2880, + }), resolution_high_minutes: Field.number({ label: 'Resolution — High (min)', defaultValue: 1440 }), - resolution_urgent_minutes: Field.number({ label: 'Resolution — Urgent (min)', defaultValue: 240 }), + resolution_urgent_minutes: Field.number({ + label: 'Resolution — Urgent (min)', + defaultValue: 240, + }), notes: Field.markdown({ label: 'Notes' }), }, diff --git a/packages/helpdesk/src/objects/helpdesk_team.object.ts b/packages/helpdesk/src/objects/helpdesk_team.object.ts index f585983..0fedda8 100644 --- a/packages/helpdesk/src/objects/helpdesk_team.object.ts +++ b/packages/helpdesk/src/objects/helpdesk_team.object.ts @@ -43,7 +43,8 @@ export const Team = ObjectSchema.create({ business_hours: Field.text({ label: 'Business Hours', maxLength: 200, - description: 'Free-form e.g. "Mon–Fri 09:00–18:00 Asia/Shanghai"; fork to a structured calendar.', + description: + 'Free-form e.g. "Mon–Fri 09:00–18:00 Asia/Shanghai"; fork to a structured calendar.', }), }, diff --git a/packages/helpdesk/src/objects/helpdesk_ticket.object.ts b/packages/helpdesk/src/objects/helpdesk_ticket.object.ts index c293df9..023c2d5 100644 --- a/packages/helpdesk/src/objects/helpdesk_ticket.object.ts +++ b/packages/helpdesk/src/objects/helpdesk_ticket.object.ts @@ -19,8 +19,7 @@ export const Ticket = ObjectSchema.create({ label: 'Ticket', pluralLabel: 'Tickets', icon: 'life-buoy', - description: - 'A customer support request. AI-triaged, SLA-tracked, threaded with messages.', + description: 'A customer support request. AI-triaged, SLA-tracked, threaded with messages.', fieldGroups: [ { key: 'core', label: 'Ticket', icon: 'life-buoy' }, @@ -259,7 +258,15 @@ export const Ticket = ObjectSchema.create({ type: 'state_machine', name: 'ticket_lifecycle', field: 'status', - transitions: {new:["triaged", "escalated"], triaged:["in_progress", "escalated"], in_progress:["waiting_customer", "resolved", "escalated"], waiting_customer:["in_progress", "resolved", "escalated"], resolved:["closed", "in_progress"], escalated:["in_progress", "resolved"], closed:[]}, + transitions: { + new: ['triaged', 'escalated'], + triaged: ['in_progress', 'escalated'], + in_progress: ['waiting_customer', 'resolved', 'escalated'], + waiting_customer: ['in_progress', 'resolved', 'escalated'], + resolved: ['closed', 'in_progress'], + escalated: ['in_progress', 'resolved'], + closed: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/helpdesk/src/portals/_portal-spec-shim.ts b/packages/helpdesk/src/portals/_portal-spec-shim.ts index 2b1fffd..e19c184 100644 --- a/packages/helpdesk/src/portals/_portal-spec-shim.ts +++ b/packages/helpdesk/src/portals/_portal-spec-shim.ts @@ -70,17 +70,9 @@ export type PortalNavItem = | (BasePortalNavItem & { type: 'dashboard'; dashboardName: string }) | (BasePortalNavItem & { type: 'url'; url: string; target?: '_self' | '_blank' }); -export type PortalAuthMode = - | 'authenticated' - | 'magic-link' - | 'anonymous' - | `sso:${string}`; - -export type PortalLayout = - | 'console' - | 'minimal' - | 'embedded' - | `custom:${string}`; +export type PortalAuthMode = 'authenticated' | 'magic-link' | 'anonymous' | `sso:${string}`; + +export type PortalLayout = 'console' | 'minimal' | 'embedded' | `custom:${string}`; export interface PortalInput { kind: 'portal'; diff --git a/packages/helpdesk/src/profiles/agent.profile.ts b/packages/helpdesk/src/profiles/agent.profile.ts index 039d5ba..c87b99a 100644 --- a/packages/helpdesk/src/profiles/agent.profile.ts +++ b/packages/helpdesk/src/profiles/agent.profile.ts @@ -8,11 +8,53 @@ export const AgentProfile = { label: 'Support Agent', isProfile: true, objects: { - helpdesk_ticket: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - helpdesk_message: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - helpdesk_customer: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - helpdesk_kb_article: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - helpdesk_team: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - helpdesk_sla_policy: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, + helpdesk_ticket: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + helpdesk_message: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + helpdesk_customer: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + helpdesk_kb_article: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + helpdesk_team: { + allowCreate: false, + allowRead: true, + allowEdit: false, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + helpdesk_sla_policy: { + allowCreate: false, + allowRead: true, + allowEdit: false, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, }, }; diff --git a/packages/helpdesk/src/profiles/customer_portal.profile.ts b/packages/helpdesk/src/profiles/customer_portal.profile.ts index 46a612f..5f24ca8 100644 --- a/packages/helpdesk/src/profiles/customer_portal.profile.ts +++ b/packages/helpdesk/src/profiles/customer_portal.profile.ts @@ -11,11 +11,53 @@ export const CustomerPortalProfile = { label: 'Customer Portal User', isProfile: true, objects: { - helpdesk_ticket: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: false, modifyAllRecords: false }, - helpdesk_message: { allowCreate: true, allowRead: true, allowEdit: false, allowDelete: false, viewAllRecords: false, modifyAllRecords: false }, - helpdesk_kb_article: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - helpdesk_customer: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false, viewAllRecords: false, modifyAllRecords: false }, - helpdesk_team: { allowCreate: false, allowRead: false, allowEdit: false, allowDelete: false, viewAllRecords: false, modifyAllRecords: false }, - helpdesk_sla_policy: { allowCreate: false, allowRead: false, allowEdit: false, allowDelete: false, viewAllRecords: false, modifyAllRecords: false }, + helpdesk_ticket: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: false, + modifyAllRecords: false, + }, + helpdesk_message: { + allowCreate: true, + allowRead: true, + allowEdit: false, + allowDelete: false, + viewAllRecords: false, + modifyAllRecords: false, + }, + helpdesk_kb_article: { + allowCreate: false, + allowRead: true, + allowEdit: false, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + helpdesk_customer: { + allowCreate: false, + allowRead: true, + allowEdit: false, + allowDelete: false, + viewAllRecords: false, + modifyAllRecords: false, + }, + helpdesk_team: { + allowCreate: false, + allowRead: false, + allowEdit: false, + allowDelete: false, + viewAllRecords: false, + modifyAllRecords: false, + }, + helpdesk_sla_policy: { + allowCreate: false, + allowRead: false, + allowEdit: false, + allowDelete: false, + viewAllRecords: false, + modifyAllRecords: false, + }, }, }; diff --git a/packages/helpdesk/src/profiles/helpdesk_admin.profile.ts b/packages/helpdesk/src/profiles/helpdesk_admin.profile.ts index 8f10d1c..7f15877 100644 --- a/packages/helpdesk/src/profiles/helpdesk_admin.profile.ts +++ b/packages/helpdesk/src/profiles/helpdesk_admin.profile.ts @@ -8,11 +8,53 @@ export const HelpdeskAdminProfile = { label: 'Helpdesk Admin', isProfile: true, objects: { - helpdesk_ticket: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - helpdesk_customer: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - helpdesk_team: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - helpdesk_kb_article: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - helpdesk_message: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - helpdesk_sla_policy: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, + helpdesk_ticket: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + helpdesk_customer: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + helpdesk_team: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + helpdesk_kb_article: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + helpdesk_message: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + helpdesk_sla_policy: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, }, }; diff --git a/packages/helpdesk/src/sharing/index.ts b/packages/helpdesk/src/sharing/index.ts index eb3e1a7..368de4c 100644 --- a/packages/helpdesk/src/sharing/index.ts +++ b/packages/helpdesk/src/sharing/index.ts @@ -10,8 +10,16 @@ export const RoleHierarchy = { roles: [ { name: 'helpdesk_admin', label: 'Helpdesk Admin', parentRole: null as string | null }, - { name: 'helpdesk_manager', label: 'Support Manager', parentRole: 'helpdesk_admin' as string | null }, - { name: 'helpdesk_agent', label: 'Support Agent', parentRole: 'helpdesk_manager' as string | null }, + { + name: 'helpdesk_manager', + label: 'Support Manager', + parentRole: 'helpdesk_admin' as string | null, + }, + { + name: 'helpdesk_agent', + label: 'Support Agent', + parentRole: 'helpdesk_manager' as string | null, + }, { name: 'helpdesk_customer', label: 'Customer Portal User', parentRole: null as string | null }, ], }; diff --git a/packages/helpdesk/src/translations/en.ts b/packages/helpdesk/src/translations/en.ts index 85ff171..c5d26e4 100644 --- a/packages/helpdesk/src/translations/en.ts +++ b/packages/helpdesk/src/translations/en.ts @@ -19,9 +19,13 @@ export const en: TranslationData = { status: { label: 'Status', options: { - new: 'New', triaged: 'Triaged', in_progress: 'In Progress', - waiting_customer: 'Waiting Customer', resolved: 'Resolved', - closed: 'Closed', escalated: 'Escalated', + new: 'New', + triaged: 'Triaged', + in_progress: 'In Progress', + waiting_customer: 'Waiting Customer', + resolved: 'Resolved', + closed: 'Closed', + escalated: 'Escalated', }, }, priority: { @@ -36,15 +40,24 @@ export const en: TranslationData = { ai_category: { label: 'AI Category', options: { - bug: 'Bug / Defect', how_to: 'How-to', billing: 'Account / Billing', - feature_request: 'Feature Request', outage: 'Outage', - feedback: 'Feedback', other: 'Other', + bug: 'Bug / Defect', + how_to: 'How-to', + billing: 'Account / Billing', + feature_request: 'Feature Request', + outage: 'Outage', + feedback: 'Feedback', + other: 'Other', }, }, ai_intent: { label: 'AI Intent' }, ai_sentiment: { label: 'AI Sentiment', - options: { positive: 'Positive', neutral: 'Neutral', frustrated: 'Frustrated', angry: 'Angry' }, + options: { + positive: 'Positive', + neutral: 'Neutral', + frustrated: 'Frustrated', + angry: 'Angry', + }, }, ai_priority_suggestion: { label: 'AI Suggested Priority', @@ -70,64 +83,118 @@ export const en: TranslationData = { all_tickets: { label: 'All Tickets', description: 'Every ticket, grouped by status' }, ticket_pipeline: { label: 'Ticket Pipeline', description: 'Kanban grouped by status' }, open_tickets: { label: 'Open Tickets', description: 'Anything not closed or resolved' }, - breaching_tickets: { label: 'Breaching SLA', description: 'Past resolution_due_at and still open' }, + breaching_tickets: { + label: 'Breaching SLA', + description: 'Past resolution_due_at and still open', + }, angry_tickets: { label: 'Angry Customers', description: 'AI detected angry sentiment' }, my_queue: { label: 'My Queue', description: 'Tickets assigned to me' }, }, }, helpdesk_customer: { - label: 'Customer', pluralLabel: 'Customers', + label: 'Customer', + pluralLabel: 'Customers', description: 'A person who files tickets.', fields: { - name: { label: 'Name' }, email: { label: 'Email' }, company: { label: 'Company' }, - tier: { label: 'Tier', options: { free: 'Free', pro: 'Pro', business: 'Business', enterprise: 'Enterprise' } }, - locale: { label: 'Preferred Locale' }, portal_user: { label: 'Portal User' }, - timezone: { label: 'Timezone' }, notes: { label: 'Notes' }, + name: { label: 'Name' }, + email: { label: 'Email' }, + company: { label: 'Company' }, + tier: { + label: 'Tier', + options: { free: 'Free', pro: 'Pro', business: 'Business', enterprise: 'Enterprise' }, + }, + locale: { label: 'Preferred Locale' }, + portal_user: { label: 'Portal User' }, + timezone: { label: 'Timezone' }, + notes: { label: 'Notes' }, }, }, helpdesk_team: { - label: 'Team', pluralLabel: 'Teams', + label: 'Team', + pluralLabel: 'Teams', description: 'A support team / queue.', fields: { - name: { label: 'Name' }, code: { label: 'Code' }, + name: { label: 'Name' }, + code: { label: 'Code' }, specialty: { label: 'Specialty', - options: { tier1: 'Tier 1 — General', tier2: 'Tier 2 — Technical', billing: 'Billing', account: 'Account Management', trust: 'Trust & Safety' }, + options: { + tier1: 'Tier 1 — General', + tier2: 'Tier 2 — Technical', + billing: 'Billing', + account: 'Account Management', + trust: 'Trust & Safety', + }, }, - manager: { label: 'Manager' }, is_active: { label: 'Active' }, business_hours: { label: 'Business Hours' }, + manager: { label: 'Manager' }, + is_active: { label: 'Active' }, + business_hours: { label: 'Business Hours' }, }, }, helpdesk_kb_article: { - label: 'KB Article', pluralLabel: 'KB Articles', + label: 'KB Article', + pluralLabel: 'KB Articles', description: 'Knowledge base article. Published articles are eligible for AI recall.', fields: { - name: { label: 'Title' }, slug: { label: 'Slug' }, body: { label: 'Body' }, + name: { label: 'Title' }, + slug: { label: 'Slug' }, + body: { label: 'Body' }, category: { label: 'Category', - options: { getting_started: 'Getting Started', billing: 'Account & Billing', how_to: 'How-to', troubleshooting: 'Troubleshooting', api: 'API & Integrations', known_issues: 'Known Issues' }, + options: { + getting_started: 'Getting Started', + billing: 'Account & Billing', + how_to: 'How-to', + troubleshooting: 'Troubleshooting', + api: 'API & Integrations', + known_issues: 'Known Issues', + }, }, - status: { label: 'Status', options: { draft: 'Draft', review: 'In Review', published: 'Published', archived: 'Archived' } }, - tags: { label: 'Tags' }, locale: { label: 'Locale' }, author: { label: 'Author' }, - helpful_count: { label: 'Helpful Votes' }, unhelpful_count: { label: 'Unhelpful Votes' }, + status: { + label: 'Status', + options: { + draft: 'Draft', + review: 'In Review', + published: 'Published', + archived: 'Archived', + }, + }, + tags: { label: 'Tags' }, + locale: { label: 'Locale' }, + author: { label: 'Author' }, + helpful_count: { label: 'Helpful Votes' }, + unhelpful_count: { label: 'Unhelpful Votes' }, published_at: { label: 'Published At' }, }, }, helpdesk_message: { - label: 'Message', pluralLabel: 'Messages', + label: 'Message', + pluralLabel: 'Messages', description: 'One message in a ticket thread.', fields: { - name: { label: 'Snippet' }, ticket: { label: 'Ticket' }, - direction: { label: 'Direction', options: { inbound: 'Inbound', outbound: 'Outbound', internal_note: 'Internal Note' } }, - author_user: { label: 'Author (User)' }, author_customer: { label: 'Author (Customer)' }, - body: { label: 'Body' }, is_ai_drafted: { label: 'AI Drafted' }, sent_at: { label: 'Sent At' }, + name: { label: 'Snippet' }, + ticket: { label: 'Ticket' }, + direction: { + label: 'Direction', + options: { inbound: 'Inbound', outbound: 'Outbound', internal_note: 'Internal Note' }, + }, + author_user: { label: 'Author (User)' }, + author_customer: { label: 'Author (Customer)' }, + body: { label: 'Body' }, + is_ai_drafted: { label: 'AI Drafted' }, + sent_at: { label: 'Sent At' }, }, }, helpdesk_sla_policy: { - label: 'SLA Policy', pluralLabel: 'SLA Policies', + label: 'SLA Policy', + pluralLabel: 'SLA Policies', description: 'Response and resolution targets, by priority.', fields: { name: { label: 'Name' }, - applies_to_tier: { label: 'Applies To Tier', options: { free: 'Free', pro: 'Pro', business: 'Business', enterprise: 'Enterprise' } }, + applies_to_tier: { + label: 'Applies To Tier', + options: { free: 'Free', pro: 'Pro', business: 'Business', enterprise: 'Enterprise' }, + }, is_default: { label: 'Default' }, first_response_low_minutes: { label: 'First Response — Low (min)' }, first_response_normal_minutes: { label: 'First Response — Normal (min)' }, diff --git a/packages/helpdesk/src/translations/zh-CN.ts b/packages/helpdesk/src/translations/zh-CN.ts index 13053ce..ecb2c5b 100644 --- a/packages/helpdesk/src/translations/zh-CN.ts +++ b/packages/helpdesk/src/translations/zh-CN.ts @@ -19,9 +19,13 @@ export const zhCN: TranslationData = { status: { label: '状态', options: { - new: '新建', triaged: '已分诊', in_progress: '处理中', - waiting_customer: '等待客户', resolved: '已解决', - closed: '已关闭', escalated: '已升级', + new: '新建', + triaged: '已分诊', + in_progress: '处理中', + waiting_customer: '等待客户', + resolved: '已解决', + closed: '已关闭', + escalated: '已升级', }, }, priority: { @@ -36,9 +40,13 @@ export const zhCN: TranslationData = { ai_category: { label: 'AI 分类', options: { - bug: '缺陷', how_to: '使用问题', billing: '账户/账单', - feature_request: '功能请求', outage: '故障', - feedback: '反馈', other: '其他', + bug: '缺陷', + how_to: '使用问题', + billing: '账户/账单', + feature_request: '功能请求', + outage: '故障', + feedback: '反馈', + other: '其他', }, }, ai_intent: { label: 'AI 意图' }, @@ -76,58 +84,104 @@ export const zhCN: TranslationData = { }, }, helpdesk_customer: { - label: '客户', pluralLabel: '客户', + label: '客户', + pluralLabel: '客户', description: '提交工单的客户。', fields: { - name: { label: '姓名' }, email: { label: '邮箱' }, company: { label: '公司' }, - tier: { label: '客户等级', options: { free: '免费', pro: '专业版', business: '商业版', enterprise: '企业版' } }, - locale: { label: '首选语言' }, portal_user: { label: '门户账号' }, - timezone: { label: '时区' }, notes: { label: '备注' }, + name: { label: '姓名' }, + email: { label: '邮箱' }, + company: { label: '公司' }, + tier: { + label: '客户等级', + options: { free: '免费', pro: '专业版', business: '商业版', enterprise: '企业版' }, + }, + locale: { label: '首选语言' }, + portal_user: { label: '门户账号' }, + timezone: { label: '时区' }, + notes: { label: '备注' }, }, }, helpdesk_team: { - label: '团队', pluralLabel: '团队', + label: '团队', + pluralLabel: '团队', description: '客服团队 / 队列。', fields: { - name: { label: '团队名称' }, code: { label: '团队代码' }, + name: { label: '团队名称' }, + code: { label: '团队代码' }, specialty: { label: '专长', - options: { tier1: '一线 — 通用', tier2: '二线 — 技术', billing: '账单', account: '客户经理', trust: '信任与安全' }, + options: { + tier1: '一线 — 通用', + tier2: '二线 — 技术', + billing: '账单', + account: '客户经理', + trust: '信任与安全', + }, }, - manager: { label: '主管' }, is_active: { label: '启用' }, business_hours: { label: '工作时间' }, + manager: { label: '主管' }, + is_active: { label: '启用' }, + business_hours: { label: '工作时间' }, }, }, helpdesk_kb_article: { - label: '知识库文章', pluralLabel: '知识库', + label: '知识库文章', + pluralLabel: '知识库', description: '知识库文章。已发布文章可被 AI 召回。', fields: { - name: { label: '标题' }, slug: { label: 'Slug' }, body: { label: '正文' }, + name: { label: '标题' }, + slug: { label: 'Slug' }, + body: { label: '正文' }, category: { label: '分类', - options: { getting_started: '入门', billing: '账户与账单', how_to: '使用指南', troubleshooting: '问题排查', api: 'API 与集成', known_issues: '已知问题' }, + options: { + getting_started: '入门', + billing: '账户与账单', + how_to: '使用指南', + troubleshooting: '问题排查', + api: 'API 与集成', + known_issues: '已知问题', + }, }, - status: { label: '状态', options: { draft: '草稿', review: '审核中', published: '已发布', archived: '已归档' } }, - tags: { label: '标签' }, locale: { label: '语言' }, author: { label: '作者' }, - helpful_count: { label: '有用票数' }, unhelpful_count: { label: '无用票数' }, + status: { + label: '状态', + options: { draft: '草稿', review: '审核中', published: '已发布', archived: '已归档' }, + }, + tags: { label: '标签' }, + locale: { label: '语言' }, + author: { label: '作者' }, + helpful_count: { label: '有用票数' }, + unhelpful_count: { label: '无用票数' }, published_at: { label: '发布时间' }, }, }, helpdesk_message: { - label: '消息', pluralLabel: '消息', + label: '消息', + pluralLabel: '消息', description: '工单线程中的一条消息。', fields: { - name: { label: '摘要' }, ticket: { label: '工单' }, - direction: { label: '方向', options: { inbound: '收到', outbound: '发出', internal_note: '内部备注' } }, - author_user: { label: '作者 (员工)' }, author_customer: { label: '作者 (客户)' }, - body: { label: '正文' }, is_ai_drafted: { label: 'AI 起草' }, sent_at: { label: '发送时间' }, + name: { label: '摘要' }, + ticket: { label: '工单' }, + direction: { + label: '方向', + options: { inbound: '收到', outbound: '发出', internal_note: '内部备注' }, + }, + author_user: { label: '作者 (员工)' }, + author_customer: { label: '作者 (客户)' }, + body: { label: '正文' }, + is_ai_drafted: { label: 'AI 起草' }, + sent_at: { label: '发送时间' }, }, }, helpdesk_sla_policy: { - label: 'SLA 策略', pluralLabel: 'SLA 策略', + label: 'SLA 策略', + pluralLabel: 'SLA 策略', description: '按优先级配置首响和解决时间目标。', fields: { name: { label: '名称' }, - applies_to_tier: { label: '适用等级', options: { free: '免费', pro: '专业版', business: '商业版', enterprise: '企业版' } }, + applies_to_tier: { + label: '适用等级', + options: { free: '免费', pro: '专业版', business: '商业版', enterprise: '企业版' }, + }, is_default: { label: '默认' }, first_response_low_minutes: { label: '首响 — 低 (分钟)' }, first_response_normal_minutes: { label: '首响 — 普通 (分钟)' }, diff --git a/packages/helpdesk/src/views/helpdesk_sla_policy.view.ts b/packages/helpdesk/src/views/helpdesk_sla_policy.view.ts index 3800ba2..127ce06 100644 --- a/packages/helpdesk/src/views/helpdesk_sla_policy.view.ts +++ b/packages/helpdesk/src/views/helpdesk_sla_policy.view.ts @@ -27,11 +27,7 @@ export const SLAPolicyViews = defineView({ { label: 'Policy', columns: 2, - fields: [ - { field: 'name', required: true, colSpan: 2 }, - 'applies_to_tier', - 'is_default', - ], + fields: [{ field: 'name', required: true, colSpan: 2 }, 'applies_to_tier', 'is_default'], }, { label: 'First Response (minutes)', diff --git a/packages/helpdesk/src/views/helpdesk_ticket.view.ts b/packages/helpdesk/src/views/helpdesk_ticket.view.ts index 1000050..cd1d69f 100644 --- a/packages/helpdesk/src/views/helpdesk_ticket.view.ts +++ b/packages/helpdesk/src/views/helpdesk_ticket.view.ts @@ -31,7 +31,12 @@ export const TicketViews = defineView({ tabs: [ { name: 'all', label: 'All', view: 'all_tickets', isDefault: true, pinned: true }, { name: 'open', label: 'Open', icon: 'inbox', view: 'open_tickets' }, - { name: 'breaching', label: 'Breaching SLA', icon: 'alert-triangle', view: 'breaching_tickets' }, + { + name: 'breaching', + label: 'Breaching SLA', + icon: 'alert-triangle', + view: 'breaching_tickets', + }, { name: 'angry', label: 'Angry', icon: 'flame', view: 'angry_tickets' }, ], }, @@ -54,9 +59,21 @@ export const TicketViews = defineView({ type: 'grid', label: 'Open Tickets', data: { provider: 'object', object: 'helpdesk_ticket' }, - columns: ['ticket_number', 'name', 'status', 'priority', 'ai_sentiment', 'assignee', 'first_response_due_at'], + columns: [ + 'ticket_number', + 'name', + 'status', + 'priority', + 'ai_sentiment', + 'assignee', + 'first_response_due_at', + ], filter: [ - { field: 'status', operator: 'in', value: ['new', 'triaged', 'in_progress', 'waiting_customer', 'escalated'] }, + { + field: 'status', + operator: 'in', + value: ['new', 'triaged', 'in_progress', 'waiting_customer', 'escalated'], + }, ], sort: [{ field: 'priority', order: 'desc' }], }, @@ -66,7 +83,14 @@ export const TicketViews = defineView({ type: 'grid', label: 'Breaching SLA', data: { provider: 'object', object: 'helpdesk_ticket' }, - columns: ['ticket_number', 'name', 'priority', 'first_response_due_at', 'resolution_due_at', 'assignee'], + columns: [ + 'ticket_number', + 'name', + 'priority', + 'first_response_due_at', + 'resolution_due_at', + 'assignee', + ], filter: [ { field: 'status', operator: 'not_in', value: ['closed', 'resolved'] }, { field: 'resolution_due_at', operator: 'less_than', value: '{today}' }, @@ -92,7 +116,14 @@ export const TicketViews = defineView({ type: 'grid', label: 'My Queue', data: { provider: 'object', object: 'helpdesk_ticket' }, - columns: ['ticket_number', 'name', 'status', 'priority', 'ai_sentiment', 'first_response_due_at'], + columns: [ + 'ticket_number', + 'name', + 'status', + 'priority', + 'ai_sentiment', + 'first_response_due_at', + ], filter: [ { field: 'assignee', operator: 'equals', value: '{currentUser}' }, { field: 'status', operator: 'not_in', value: ['closed', 'resolved'] }, @@ -153,7 +184,12 @@ export const TicketViews = defineView({ { label: 'CSAT & Notes', columns: 2, - fields: ['csat_score', 'csat_comment', { field: 'tags', colSpan: 2 }, { field: 'internal_notes', colSpan: 2 }], + fields: [ + 'csat_score', + 'csat_comment', + { field: 'tags', colSpan: 2 }, + { field: 'internal_notes', colSpan: 2 }, + ], }, ], }, diff --git a/packages/hr/objectstack.manifest.json b/packages/hr/objectstack.manifest.json index 7190477..239f7a7 100644 --- a/packages/hr/objectstack.manifest.json +++ b/packages/hr/objectstack.manifest.json @@ -13,7 +13,12 @@ "iconUrl": "https://cdn.objectos.app/icons/hr.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/hr", "tags": ["hr", "hris", "people", "time-off"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md", "translations": { "zh-CN": { diff --git a/packages/hr/src/apps/hr.app.ts b/packages/hr/src/apps/hr.app.ts index b1b40d9..76f932f 100644 --- a/packages/hr/src/apps/hr.app.ts +++ b/packages/hr/src/apps/hr.app.ts @@ -17,9 +17,33 @@ export const HrApp = App.create({ label: 'HR Dashboard', icon: 'gauge', }, - { id: 'nav_employee', type: 'object', objectName: 'hr_employee', label: 'Employees', icon: 'user' }, - { id: 'nav_department', type: 'object', objectName: 'hr_department', label: 'Departments', icon: 'building' }, - { id: 'nav_time_off', type: 'object', objectName: 'hr_time_off_request', label: 'Time-Off', icon: 'calendar-off' }, - { id: 'nav_document', type: 'object', objectName: 'hr_document', label: 'Documents', icon: 'file-text' }, + { + id: 'nav_employee', + type: 'object', + objectName: 'hr_employee', + label: 'Employees', + icon: 'user', + }, + { + id: 'nav_department', + type: 'object', + objectName: 'hr_department', + label: 'Departments', + icon: 'building', + }, + { + id: 'nav_time_off', + type: 'object', + objectName: 'hr_time_off_request', + label: 'Time-Off', + icon: 'calendar-off', + }, + { + id: 'nav_document', + type: 'object', + objectName: 'hr_document', + label: 'Documents', + icon: 'file-text', + }, ], }); diff --git a/packages/hr/src/data/index.ts b/packages/hr/src/data/index.ts index 444281d..6abb575 100644 --- a/packages/hr/src/data/index.ts +++ b/packages/hr/src/data/index.ts @@ -21,7 +21,12 @@ const departments = defineDataset(Department, { externalId: 'name', records: [ { name: 'Engineering', code: 'ENG', description: 'Builds and runs the product.' }, - { name: 'Platform', code: 'ENG-PLAT', parent: 'Engineering', description: 'Infrastructure and developer experience.' }, + { + name: 'Platform', + code: 'ENG-PLAT', + parent: 'Engineering', + description: 'Infrastructure and developer experience.', + }, { name: 'Marketing', code: 'MKT', description: 'Brand, content, and growth.' }, { name: 'People Ops', code: 'PPL', description: 'HR, recruiting, and culture.' }, ], diff --git a/packages/hr/src/objects/hr_department.object.ts b/packages/hr/src/objects/hr_department.object.ts index e924591..ef43a33 100644 --- a/packages/hr/src/objects/hr_department.object.ts +++ b/packages/hr/src/objects/hr_department.object.ts @@ -49,10 +49,7 @@ export const Department = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['name'], unique: true }, - { fields: ['parent'] }, - ], + indexes: [{ fields: ['name'], unique: true }, { fields: ['parent'] }], titleFormat: tmpl`{{record.name}}`, compactLayout: ['name', 'code', 'parent', 'head'], diff --git a/packages/hr/src/objects/hr_document.object.ts b/packages/hr/src/objects/hr_document.object.ts index 3b4b79d..410b825 100644 --- a/packages/hr/src/objects/hr_document.object.ts +++ b/packages/hr/src/objects/hr_document.object.ts @@ -67,11 +67,7 @@ export const EmployeeDocument = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['employee'] }, - { fields: ['doc_type'] }, - { fields: ['expires_at'] }, - ], + indexes: [{ fields: ['employee'] }, { fields: ['doc_type'] }, { fields: ['expires_at'] }], titleFormat: tmpl`{{record.name}}`, compactLayout: ['name', 'employee', 'doc_type', 'expires_at'], diff --git a/packages/hr/src/objects/hr_time_off_request.object.ts b/packages/hr/src/objects/hr_time_off_request.object.ts index 78ea8ad..4e71c1b 100644 --- a/packages/hr/src/objects/hr_time_off_request.object.ts +++ b/packages/hr/src/objects/hr_time_off_request.object.ts @@ -44,7 +44,8 @@ export const TimeOffRequest = ObjectSchema.create({ days: Field.formula({ label: 'Calendar Days', group: 'core', - description: 'Inclusive calendar-day span (weekends + holidays counted). Switch to a working-days helper if your policy excludes them.', + description: + 'Inclusive calendar-day span (weekends + holidays counted). Switch to a working-days helper if your policy excludes them.', expression: F`record.start_date != null && record.end_date != null ? (record.end_date - record.start_date) + 1 : null`, }), reason: Field.markdown({ @@ -94,11 +95,7 @@ export const TimeOffRequest = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['employee'] }, - { fields: ['status'] }, - { fields: ['start_date'] }, - ], + indexes: [{ fields: ['employee'] }, { fields: ['status'] }, { fields: ['start_date'] }], titleFormat: tmpl`{{record.leave_type}} · {{record.start_date}} → {{record.end_date}}`, compactLayout: ['employee', 'leave_type', 'start_date', 'end_date', 'status'], @@ -108,7 +105,13 @@ export const TimeOffRequest = ObjectSchema.create({ type: 'state_machine', name: 'time_off_lifecycle', field: 'status', - transitions: {draft:["submitted", "cancelled"], submitted:["approved", "rejected", "draft"], approved:["cancelled"], rejected:["draft"], cancelled:[]}, + transitions: { + draft: ['submitted', 'cancelled'], + submitted: ['approved', 'rejected', 'draft'], + approved: ['cancelled'], + rejected: ['draft'], + cancelled: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/hr/src/translations/en.ts b/packages/hr/src/translations/en.ts index 09232a5..119cd38 100644 --- a/packages/hr/src/translations/en.ts +++ b/packages/hr/src/translations/en.ts @@ -40,7 +40,12 @@ export const en: TranslationData = { }, employment_type: { label: 'Employment Type', - options: { full_time: 'Full-time', part_time: 'Part-time', contractor: 'Contractor', intern: 'Intern' }, + options: { + full_time: 'Full-time', + part_time: 'Part-time', + contractor: 'Contractor', + intern: 'Intern', + }, }, hire_date: { label: 'Hire Date' }, end_date: { label: 'End Date' }, @@ -51,7 +56,10 @@ export const en: TranslationData = { notes: { label: 'Internal Notes' }, }, _views: { - all_employees: { label: 'All Employees', description: 'Every employee grouped by department' }, + all_employees: { + label: 'All Employees', + description: 'Every employee grouped by department', + }, active_employees: { label: 'Active', description: 'Currently active staff' }, employees_on_leave: { label: 'On Leave', description: 'Staff currently on leave' }, }, @@ -89,7 +97,10 @@ export const en: TranslationData = { _views: { all_time_off: { label: 'All Requests', description: 'Every request grouped by status' }, time_off_pipeline: { label: 'Approval Pipeline', description: 'Kanban grouped by status' }, - pending_time_off: { label: 'Pending Approval', description: 'Submitted requests awaiting decision' }, + pending_time_off: { + label: 'Pending Approval', + description: 'Submitted requests awaiting decision', + }, approved_time_off: { label: 'Approved', description: 'Approved requests by start date' }, }, }, @@ -119,8 +130,14 @@ export const en: TranslationData = { }, _views: { all_documents: { label: 'All Documents', description: 'Every document grouped by type' }, - expiring_documents: { label: 'Expiring Soon', description: 'Documents expiring within 30 days' }, - expired_documents: { label: 'Expired', description: 'Documents already past their expiry date' }, + expiring_documents: { + label: 'Expiring Soon', + description: 'Documents expiring within 30 days', + }, + expired_documents: { + label: 'Expired', + description: 'Documents already past their expiry date', + }, }, }, }, @@ -168,10 +185,22 @@ export const en: TranslationData = { widgets: { headcount: { title: 'Active Headcount', description: 'Employees in active status' }, on_leave: { title: 'On Leave', description: 'Employees currently on leave' }, - pending_time_off: { title: 'Pending Approvals', description: 'Time-off requests awaiting manager decision' }, - expiring_docs: { title: 'Documents Expiring (30d)', description: 'Documents whose expires_at falls in the next 30 days' }, - pending_time_off_table: { title: 'Pending Time-Off Requests', description: 'Submitted requests, oldest first' }, - expiring_docs_table: { title: 'Documents Expiring Soon', description: 'Documents expiring within 30 days' }, + pending_time_off: { + title: 'Pending Approvals', + description: 'Time-off requests awaiting manager decision', + }, + expiring_docs: { + title: 'Documents Expiring (30d)', + description: 'Documents whose expires_at falls in the next 30 days', + }, + pending_time_off_table: { + title: 'Pending Time-Off Requests', + description: 'Submitted requests, oldest first', + }, + expiring_docs_table: { + title: 'Documents Expiring Soon', + description: 'Documents expiring within 30 days', + }, }, }, }, diff --git a/packages/hr/src/translations/zh-CN.ts b/packages/hr/src/translations/zh-CN.ts index 0d03125..d3c35aa 100644 --- a/packages/hr/src/translations/zh-CN.ts +++ b/packages/hr/src/translations/zh-CN.ts @@ -172,7 +172,10 @@ export const zhCN: TranslationData = { headcount: { title: '在职人数', description: '当前在职状态的员工总数' }, on_leave: { title: '休假中人数', description: '当前正在休假的员工' }, pending_time_off: { title: '待审批请假', description: '等待直属上级决策的请假申请' }, - expiring_docs: { title: '档案即将到期(30 天)', description: '到期日落在未来 30 天内的档案' }, + expiring_docs: { + title: '档案即将到期(30 天)', + description: '到期日落在未来 30 天内的档案', + }, pending_time_off_table: { title: '待处理请假申请', description: '按提交时间从旧到新排序' }, expiring_docs_table: { title: '即将到期的档案', description: '30 天内到期的档案' }, }, diff --git a/packages/hr/src/views/hr_document.view.ts b/packages/hr/src/views/hr_document.view.ts index d852a59..f6eba0e 100644 --- a/packages/hr/src/views/hr_document.view.ts +++ b/packages/hr/src/views/hr_document.view.ts @@ -24,7 +24,12 @@ export const EmployeeDocumentViews = defineView({ appearance: { allowedVisualizations: ['grid'] }, tabs: [ { name: 'all', label: 'All', view: 'all_documents', isDefault: true, pinned: true }, - { name: 'expiring', label: 'Expiring Soon', icon: 'alert-triangle', view: 'expiring_documents' }, + { + name: 'expiring', + label: 'Expiring Soon', + icon: 'alert-triangle', + view: 'expiring_documents', + }, { name: 'expired', label: 'Expired', icon: 'x-circle', view: 'expired_documents' }, ], }, diff --git a/packages/hr/src/views/hr_employee.view.ts b/packages/hr/src/views/hr_employee.view.ts index a3cf857..a4f0c44 100644 --- a/packages/hr/src/views/hr_employee.view.ts +++ b/packages/hr/src/views/hr_employee.view.ts @@ -28,7 +28,12 @@ export const EmployeeViews = defineView({ { name: 'all', label: 'All', view: 'all_employees', isDefault: true, pinned: true }, { name: 'active', label: 'Active', icon: 'user-check', view: 'active_employees' }, { name: 'on_leave', label: 'On Leave', icon: 'calendar-off', view: 'employees_on_leave' }, - { name: 'new_hires', label: 'New Hires (30d)', icon: 'user-plus', view: 'new_hires_employees' }, + { + name: 'new_hires', + label: 'New Hires (30d)', + icon: 'user-plus', + view: 'new_hires_employees', + }, { name: 'terminated', label: 'Terminated', icon: 'user-x', view: 'terminated_employees' }, ], }, diff --git a/packages/hr/src/views/hr_time_off_request.view.ts b/packages/hr/src/views/hr_time_off_request.view.ts index c19d9dc..10b52fb 100644 --- a/packages/hr/src/views/hr_time_off_request.view.ts +++ b/packages/hr/src/views/hr_time_off_request.view.ts @@ -24,7 +24,14 @@ export const TimeOffRequestViews = defineView({ exportOptions: ['csv', 'xlsx'], appearance: { allowedVisualizations: ['grid', 'kanban'] }, tabs: [ - { name: 'pending', label: 'Pending', icon: 'clock', view: 'pending_time_off', isDefault: true, pinned: true }, + { + name: 'pending', + label: 'Pending', + icon: 'clock', + view: 'pending_time_off', + isDefault: true, + pinned: true, + }, { name: 'pipeline', label: 'Approval Pipeline', icon: 'columns', view: 'time_off_pipeline' }, { name: 'approved', label: 'Approved', icon: 'check', view: 'approved_time_off' }, { name: 'all', label: 'All', view: 'all_time_off' }, @@ -57,7 +64,15 @@ export const TimeOffRequestViews = defineView({ type: 'grid', label: 'Approved', data: { provider: 'object', object: 'hr_time_off_request' }, - columns: ['employee', 'leave_type', 'start_date', 'end_date', 'days', 'approver', 'decided_at'], + columns: [ + 'employee', + 'leave_type', + 'start_date', + 'end_date', + 'days', + 'approver', + 'decided_at', + ], filter: [{ field: 'status', operator: 'equals', value: 'approved' }], sort: [{ field: 'start_date', order: 'asc' }], }, diff --git a/packages/procurement/objectstack.manifest.json b/packages/procurement/objectstack.manifest.json index fae6365..1e6c602 100644 --- a/packages/procurement/objectstack.manifest.json +++ b/packages/procurement/objectstack.manifest.json @@ -13,7 +13,12 @@ "iconUrl": "https://cdn.objectos.app/icons/procurement.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/procurement", "tags": ["procurement", "source-to-pay", "vendors", "purchase-orders"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md", "translations": { "zh-CN": { @@ -23,4 +28,3 @@ } } } - diff --git a/packages/procurement/src/apps/procurement.app.ts b/packages/procurement/src/apps/procurement.app.ts index 3bc6073..4d3e256 100644 --- a/packages/procurement/src/apps/procurement.app.ts +++ b/packages/procurement/src/apps/procurement.app.ts @@ -17,9 +17,33 @@ export const ProcurementApp = App.create({ label: 'Spend at a Glance', icon: 'gauge', }, - { id: 'nav_request', type: 'object', objectName: 'procurement_request', label: 'Requests', icon: 'shopping-cart' }, - { id: 'nav_order', type: 'object', objectName: 'procurement_order', label: 'Orders', icon: 'file-check' }, - { id: 'nav_receipt', type: 'object', objectName: 'procurement_receipt', label: 'Receipts', icon: 'package' }, - { id: 'nav_vendor', type: 'object', objectName: 'procurement_vendor', label: 'Vendors', icon: 'building' }, + { + id: 'nav_request', + type: 'object', + objectName: 'procurement_request', + label: 'Requests', + icon: 'shopping-cart', + }, + { + id: 'nav_order', + type: 'object', + objectName: 'procurement_order', + label: 'Orders', + icon: 'file-check', + }, + { + id: 'nav_receipt', + type: 'object', + objectName: 'procurement_receipt', + label: 'Receipts', + icon: 'package', + }, + { + id: 'nav_vendor', + type: 'object', + objectName: 'procurement_vendor', + label: 'Vendors', + icon: 'building', + }, ], }); diff --git a/packages/procurement/src/data/index.ts b/packages/procurement/src/data/index.ts index 6738d64..87b981e 100644 --- a/packages/procurement/src/data/index.ts +++ b/packages/procurement/src/data/index.ts @@ -72,7 +72,7 @@ const requests = defineDataset(PurchaseRequest, { request_number: 'PR-2026-0001', vendor: 'Cloudwell Hosting, Inc.', category: 'saas', - status: 'approved', // → triggers PR→PO convert flow + status: 'approved', // → triggers PR→PO convert flow estimated_amount: 180000, needed_by: cel`daysFromNow(30)`, cost_center: 'ENG-INFRA', @@ -83,7 +83,7 @@ const requests = defineDataset(PurchaseRequest, { request_number: 'PR-2026-0002', vendor: 'Dell Technologies', category: 'hardware', - status: 'submitted', // → over $5k → triggers approval flow + status: 'submitted', // → over $5k → triggers approval flow estimated_amount: 7500, needed_by: cel`daysFromNow(14)`, cost_center: 'PEOPLE-OPS', diff --git a/packages/procurement/src/flows/index.ts b/packages/procurement/src/flows/index.ts index 52b70f8..c7b748a 100644 --- a/packages/procurement/src/flows/index.ts +++ b/packages/procurement/src/flows/index.ts @@ -4,8 +4,4 @@ import { PRApprovalRequiredFlow } from './pr_approval_required.flow'; import { PRToPOConvertFlow } from './pr_to_po_convert.flow'; import { POOverdueFlow } from './po_overdue.flow'; -export const allFlows = [ - PRApprovalRequiredFlow, - PRToPOConvertFlow, - POOverdueFlow, -]; +export const allFlows = [PRApprovalRequiredFlow, PRToPOConvertFlow, POOverdueFlow]; diff --git a/packages/procurement/src/objects/procurement_order.object.ts b/packages/procurement/src/objects/procurement_order.object.ts index b1d4b99..fda9af9 100644 --- a/packages/procurement/src/objects/procurement_order.object.ts +++ b/packages/procurement/src/objects/procurement_order.object.ts @@ -145,7 +145,14 @@ export const PurchaseOrder = ObjectSchema.create({ type: 'state_machine', name: 'po_lifecycle', field: 'status', - transitions: {draft:["sent"], sent:["partial", "received", "cancelled"], partial:["received", "closed"], received:["closed"], closed:[], cancelled:[]}, + transitions: { + draft: ['sent'], + sent: ['partial', 'received', 'cancelled'], + partial: ['received', 'closed'], + received: ['closed'], + closed: [], + cancelled: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/procurement/src/objects/procurement_receipt.object.ts b/packages/procurement/src/objects/procurement_receipt.object.ts index 3db318f..48e7e44 100644 --- a/packages/procurement/src/objects/procurement_receipt.object.ts +++ b/packages/procurement/src/objects/procurement_receipt.object.ts @@ -61,11 +61,7 @@ export const GoodsReceipt = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['purchase_order'] }, - { fields: ['received_at'] }, - { fields: ['quality'] }, - ], + indexes: [{ fields: ['purchase_order'] }, { fields: ['received_at'] }, { fields: ['quality'] }], titleFormat: tmpl`Receipt {{record.receipt_number}}`, compactLayout: ['receipt_number', 'purchase_order', 'quality', 'received_value', 'received_at'], diff --git a/packages/procurement/src/objects/procurement_request.object.ts b/packages/procurement/src/objects/procurement_request.object.ts index 5ec5009..194a2f1 100644 --- a/packages/procurement/src/objects/procurement_request.object.ts +++ b/packages/procurement/src/objects/procurement_request.object.ts @@ -131,7 +131,13 @@ export const PurchaseRequest = ObjectSchema.create({ type: 'state_machine', name: 'pr_lifecycle', field: 'status', - transitions: {draft:["submitted"], submitted:["approved", "rejected", "draft"], approved:["converted"], rejected:["draft"], converted:[]}, + transitions: { + draft: ['submitted'], + submitted: ['approved', 'rejected', 'draft'], + approved: ['converted'], + rejected: ['draft'], + converted: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/procurement/src/objects/procurement_vendor.object.ts b/packages/procurement/src/objects/procurement_vendor.object.ts index 8887ed7..4963d4d 100644 --- a/packages/procurement/src/objects/procurement_vendor.object.ts +++ b/packages/procurement/src/objects/procurement_vendor.object.ts @@ -13,8 +13,7 @@ export const Vendor = ObjectSchema.create({ label: 'Vendor', pluralLabel: 'Vendors', icon: 'building', - description: - 'A supplier you place purchase orders against. Vendor master record.', + description: 'A supplier you place purchase orders against. Vendor master record.', fields: { name: Field.text({ @@ -77,11 +76,7 @@ export const Vendor = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['name'] }, - { fields: ['status'] }, - { fields: ['category'] }, - ], + indexes: [{ fields: ['name'] }, { fields: ['status'] }, { fields: ['category'] }], titleFormat: tmpl`{{record.name}}`, displayNameField: 'name', diff --git a/packages/procurement/src/profiles/buyer.profile.ts b/packages/procurement/src/profiles/buyer.profile.ts index 0dcdd3e..c52f905 100644 --- a/packages/procurement/src/profiles/buyer.profile.ts +++ b/packages/procurement/src/profiles/buyer.profile.ts @@ -9,9 +9,37 @@ export const BuyerProfile = { label: 'Buyer', isProfile: true, objects: { - procurement_vendor: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - procurement_request: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - procurement_order: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, - procurement_receipt: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, viewAllRecords: true, modifyAllRecords: false }, + procurement_vendor: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + procurement_request: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + procurement_order: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, + procurement_receipt: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: false, + viewAllRecords: true, + modifyAllRecords: false, + }, }, }; diff --git a/packages/procurement/src/profiles/procurement_admin.profile.ts b/packages/procurement/src/profiles/procurement_admin.profile.ts index 9c63022..c168d8a 100644 --- a/packages/procurement/src/profiles/procurement_admin.profile.ts +++ b/packages/procurement/src/profiles/procurement_admin.profile.ts @@ -9,9 +9,37 @@ export const ProcurementAdminProfile = { label: 'Procurement Admin', isProfile: true, objects: { - procurement_vendor: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - procurement_request: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - procurement_order: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, - procurement_receipt: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, viewAllRecords: true, modifyAllRecords: true }, + procurement_vendor: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + procurement_request: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + procurement_order: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, + procurement_receipt: { + allowCreate: true, + allowRead: true, + allowEdit: true, + allowDelete: true, + viewAllRecords: true, + modifyAllRecords: true, + }, }, }; diff --git a/packages/procurement/src/translations/en.ts b/packages/procurement/src/translations/en.ts index b73340f..8d726f0 100644 --- a/packages/procurement/src/translations/en.ts +++ b/packages/procurement/src/translations/en.ts @@ -36,7 +36,13 @@ export const en: TranslationData = { }, default_payment_terms: { label: 'Default Payment Terms', - options: { net_15: 'Net 15', net_30: 'Net 30', net_45: 'Net 45', net_60: 'Net 60', upfront: 'Upfront' }, + options: { + net_15: 'Net 15', + net_30: 'Net 30', + net_45: 'Net 45', + net_60: 'Net 60', + upfront: 'Upfront', + }, }, country: { label: 'Country' }, website: { label: 'Website' }, @@ -58,8 +64,12 @@ export const en: TranslationData = { category: { label: 'Category', options: { - saas: 'Software / SaaS', hardware: 'Hardware', services: 'Professional Services', - marketing: 'Marketing', facilities: 'Facilities', other: 'Other', + saas: 'Software / SaaS', + hardware: 'Hardware', + services: 'Professional Services', + marketing: 'Marketing', + facilities: 'Facilities', + other: 'Other', }, }, status: { @@ -84,14 +94,18 @@ export const en: TranslationData = { all_requests: { label: 'All Requests', description: 'Every request, grouped by status' }, request_pipeline: { label: 'Request Pipeline', description: 'Kanban grouped by status' }, my_requests: { label: 'My Requests', description: 'Requests where you are the requester' }, - awaiting_approval: { label: 'Awaiting Approval', description: 'Submitted PRs above the approval threshold' }, + awaiting_approval: { + label: 'Awaiting Approval', + description: 'Submitted PRs above the approval threshold', + }, }, }, procurement_order: { label: 'Purchase Order', pluralLabel: 'Purchase Orders', - description: 'A commitment to a vendor. Created from an approved PR; closed when fully received.', + description: + 'A commitment to a vendor. Created from an approved PR; closed when fully received.', fields: { po_number: { label: 'PO Number' }, vendor: { label: 'Vendor' }, @@ -99,8 +113,12 @@ export const en: TranslationData = { status: { label: 'Status', options: { - draft: 'Draft', sent: 'Sent', partial: 'Partial Receipt', - received: 'Received', closed: 'Closed', cancelled: 'Cancelled', + draft: 'Draft', + sent: 'Sent', + partial: 'Partial Receipt', + received: 'Received', + closed: 'Closed', + cancelled: 'Cancelled', }, }, owner: { label: 'Buyer' }, @@ -108,7 +126,13 @@ export const en: TranslationData = { received_amount: { label: 'Received Amount' }, payment_terms: { label: 'Payment Terms', - options: { net_15: 'Net 15', net_30: 'Net 30', net_45: 'Net 45', net_60: 'Net 60', upfront: 'Upfront' }, + options: { + net_15: 'Net 15', + net_30: 'Net 30', + net_45: 'Net 45', + net_60: 'Net 60', + upfront: 'Upfront', + }, }, is_fully_received: { label: 'Fully Received' }, order_date: { label: 'Order Date' }, @@ -187,12 +211,24 @@ export const en: TranslationData = { description: 'Open POs, MTD commitments, requests awaiting approval.', actions: { create_request: { label: 'New Request' } }, widgets: { - awaiting_approval: { title: 'PRs Awaiting Approval', description: 'Submitted PRs above the approval threshold' }, + awaiting_approval: { + title: 'PRs Awaiting Approval', + description: 'Submitted PRs above the approval threshold', + }, open_pos: { title: 'Open Purchase Orders', description: 'Sent or partially-received POs' }, - overdue_pos: { title: 'Overdue Deliveries', description: 'Open POs past expected_delivery' }, + overdue_pos: { + title: 'Overdue Deliveries', + description: 'Open POs past expected_delivery', + }, open_commitment: { title: 'Open Commitment ($)', description: 'Total value of open POs' }, - pending_requests_table: { title: 'Requests Awaiting Approval', description: 'Submitted PRs sorted by amount' }, - open_pos_table: { title: 'Open Purchase Orders', description: 'Open POs sorted by expected delivery' }, + pending_requests_table: { + title: 'Requests Awaiting Approval', + description: 'Submitted PRs sorted by amount', + }, + open_pos_table: { + title: 'Open Purchase Orders', + description: 'Open POs sorted by expected delivery', + }, }, }, }, diff --git a/packages/procurement/src/translations/zh-CN.ts b/packages/procurement/src/translations/zh-CN.ts index 59be642..05d6679 100644 --- a/packages/procurement/src/translations/zh-CN.ts +++ b/packages/procurement/src/translations/zh-CN.ts @@ -17,17 +17,32 @@ export const zhCN: TranslationData = { category: { label: '类别', options: { - saas: '软件 / SaaS', hardware: '硬件', services: '专业服务', - marketing: '市场营销', facilities: '行政设施', other: '其他', + saas: '软件 / SaaS', + hardware: '硬件', + services: '专业服务', + marketing: '市场营销', + facilities: '行政设施', + other: '其他', }, }, status: { label: '状态', - options: { active: '生效', onboarding: '入驻中', suspended: '已暂停', archived: '已归档' }, + options: { + active: '生效', + onboarding: '入驻中', + suspended: '已暂停', + archived: '已归档', + }, }, default_payment_terms: { label: '默认付款条款', - options: { net_15: '15 天', net_30: '30 天', net_45: '45 天', net_60: '60 天', upfront: '预付' }, + options: { + net_15: '15 天', + net_30: '30 天', + net_45: '45 天', + net_60: '60 天', + upfront: '预付', + }, }, country: { label: '国家/地区' }, website: { label: '网站' }, @@ -49,15 +64,22 @@ export const zhCN: TranslationData = { category: { label: '类别', options: { - saas: '软件 / SaaS', hardware: '硬件', services: '专业服务', - marketing: '市场营销', facilities: '行政设施', other: '其他', + saas: '软件 / SaaS', + hardware: '硬件', + services: '专业服务', + marketing: '市场营销', + facilities: '行政设施', + other: '其他', }, }, status: { label: '状态', options: { - draft: '草稿', submitted: '已提交', approved: '已批准', - rejected: '已驳回', converted: '已转为订单', + draft: '草稿', + submitted: '已提交', + approved: '已批准', + rejected: '已驳回', + converted: '已转为订单', }, }, justification: { label: '业务理由' }, @@ -87,8 +109,12 @@ export const zhCN: TranslationData = { status: { label: '状态', options: { - draft: '草稿', sent: '已发送', partial: '部分到货', - received: '已收货', closed: '已关闭', cancelled: '已取消', + draft: '草稿', + sent: '已发送', + partial: '部分到货', + received: '已收货', + closed: '已关闭', + cancelled: '已取消', }, }, owner: { label: '采购员' }, @@ -96,7 +122,13 @@ export const zhCN: TranslationData = { received_amount: { label: '已收货金额' }, payment_terms: { label: '付款条款', - options: { net_15: '15 天', net_30: '30 天', net_45: '45 天', net_60: '60 天', upfront: '预付' }, + options: { + net_15: '15 天', + net_30: '30 天', + net_45: '45 天', + net_60: '60 天', + upfront: '预付', + }, }, is_fully_received: { label: '已全部收货' }, order_date: { label: '下单日期' }, diff --git a/packages/procurement/src/views/procurement_order.view.ts b/packages/procurement/src/views/procurement_order.view.ts index 5d54503..a0d5a56 100644 --- a/packages/procurement/src/views/procurement_order.view.ts +++ b/packages/procurement/src/views/procurement_order.view.ts @@ -49,10 +49,15 @@ export const PurchaseOrderViews = defineView({ type: 'grid', label: 'Open Orders', data: { provider: 'object', object: 'procurement_order' }, - columns: ['po_number', 'vendor', 'status', 'total_amount', 'received_amount', 'expected_delivery'], - filter: [ - { field: 'status', operator: 'in', value: ['sent', 'partial'] }, + columns: [ + 'po_number', + 'vendor', + 'status', + 'total_amount', + 'received_amount', + 'expected_delivery', ], + filter: [{ field: 'status', operator: 'in', value: ['sent', 'partial'] }], sort: [{ field: 'expected_delivery', order: 'asc' }], }, diff --git a/packages/procurement/src/views/procurement_request.view.ts b/packages/procurement/src/views/procurement_request.view.ts index 68b0e41..848fc5e 100644 --- a/packages/procurement/src/views/procurement_request.view.ts +++ b/packages/procurement/src/views/procurement_request.view.ts @@ -50,9 +50,7 @@ export const PurchaseRequestViews = defineView({ label: 'My Requests', data: { provider: 'object', object: 'procurement_request' }, columns: ['title', 'vendor', 'category', 'status', 'estimated_amount', 'needed_by'], - filter: [ - { field: 'requester', operator: 'equals', value: '{current_user_id}' }, - ], + filter: [{ field: 'requester', operator: 'equals', value: '{current_user_id}' }], sort: [{ field: 'needed_by', order: 'asc' }], }, diff --git a/packages/project/objectstack.manifest.json b/packages/project/objectstack.manifest.json index 64b4ca9..e41e189 100644 --- a/packages/project/objectstack.manifest.json +++ b/packages/project/objectstack.manifest.json @@ -13,6 +13,11 @@ "iconUrl": "https://api.iconify.design/lucide:folder-kanban.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/project", "tags": ["project-management", "pmo", "risk", "ai", "forecasting"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md" } diff --git a/packages/project/src/data/index.ts b/packages/project/src/data/index.ts index 40fac99..74f0b3b 100644 --- a/packages/project/src/data/index.ts +++ b/packages/project/src/data/index.ts @@ -16,10 +16,4 @@ import { resources } from './resources.data.js'; * * Timesheets are intentionally NOT seeded (high-volume transactional data). */ -export const ProjectSeedData = [ - projects, - milestones, - risks, - issues, - resources, -]; +export const ProjectSeedData = [projects, milestones, risks, issues, resources]; diff --git a/packages/project/src/data/projects.data.ts b/packages/project/src/data/projects.data.ts index 53ded87..91b220e 100644 --- a/packages/project/src/data/projects.data.ts +++ b/packages/project/src/data/projects.data.ts @@ -19,7 +19,8 @@ export const projects = defineDataset(Project, { records: [ { name: 'Mobile App Redesign', - description: 'Complete redesign of iOS and Android apps with new brand identity and improved UX. Target Q3 launch.', + description: + 'Complete redesign of iOS and Android apps with new brand identity and improved UX. Target Q3 launch.', status: 'active', priority: 'high', project_type: 'client', @@ -41,7 +42,8 @@ export const projects = defineDataset(Project, { }, { name: 'ERP System Migration', - description: 'Migrate legacy ERP to cloud-based solution. Complex data migration with high business risk.', + description: + 'Migrate legacy ERP to cloud-based solution. Complex data migration with high business risk.', status: 'at_risk', priority: 'critical', project_type: 'internal', diff --git a/packages/project/src/objects/pm_issue.object.ts b/packages/project/src/objects/pm_issue.object.ts index e26e571..46af804 100644 --- a/packages/project/src/objects/pm_issue.object.ts +++ b/packages/project/src/objects/pm_issue.object.ts @@ -14,9 +14,7 @@ export const Issue = ObjectSchema.create({ icon: 'octagon-x', description: 'A current problem that requires resolution.', - fieldGroups: [ - { key: 'core', label: 'Issue Details', icon: 'octagon-x', defaultExpanded: true }, - ], + fieldGroups: [{ key: 'core', label: 'Issue Details', icon: 'octagon-x', defaultExpanded: true }], fields: { name: Field.text({ @@ -87,11 +85,7 @@ export const Issue = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['project'] }, - { fields: ['status'] }, - { fields: ['severity'] }, - ], + indexes: [{ fields: ['project'] }, { fields: ['status'] }, { fields: ['severity'] }], titleFormat: tmpl`{{record.name}}`, displayNameField: 'name', diff --git a/packages/project/src/objects/pm_milestone.object.ts b/packages/project/src/objects/pm_milestone.object.ts index 0772b1c..cf8dd2e 100644 --- a/packages/project/src/objects/pm_milestone.object.ts +++ b/packages/project/src/objects/pm_milestone.object.ts @@ -82,11 +82,7 @@ export const Milestone = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['project'] }, - { fields: ['status'] }, - { fields: ['planned_date'] }, - ], + indexes: [{ fields: ['project'] }, { fields: ['status'] }, { fields: ['planned_date'] }], titleFormat: tmpl`{{record.name}}`, displayNameField: 'name', diff --git a/packages/project/src/objects/pm_project.object.ts b/packages/project/src/objects/pm_project.object.ts index 0d281ad..0da0fe3 100644 --- a/packages/project/src/objects/pm_project.object.ts +++ b/packages/project/src/objects/pm_project.object.ts @@ -210,7 +210,14 @@ export const Project = ObjectSchema.create({ type: 'state_machine', name: 'project_lifecycle', field: 'status', - transitions: {planning:["active", "cancelled"], active:["at_risk", "on_hold", "completed", "cancelled"], at_risk:["active", "on_hold", "completed", "cancelled"], on_hold:["active", "cancelled"], completed:[], cancelled:[]}, + transitions: { + planning: ['active', 'cancelled'], + active: ['at_risk', 'on_hold', 'completed', 'cancelled'], + at_risk: ['active', 'on_hold', 'completed', 'cancelled'], + on_hold: ['active', 'cancelled'], + completed: [], + cancelled: [], + }, message: 'Illegal status transition.', }, ], diff --git a/packages/project/src/objects/pm_resource.object.ts b/packages/project/src/objects/pm_resource.object.ts index d8ef835..f21e3ca 100644 --- a/packages/project/src/objects/pm_resource.object.ts +++ b/packages/project/src/objects/pm_resource.object.ts @@ -14,9 +14,7 @@ export const Resource = ObjectSchema.create({ icon: 'users', description: 'Team member or budget allocated to a project.', - fieldGroups: [ - { key: 'core', label: 'Allocation', icon: 'users', defaultExpanded: true }, - ], + fieldGroups: [{ key: 'core', label: 'Allocation', icon: 'users', defaultExpanded: true }], fields: { project: Field.lookup('pm_project', { @@ -63,11 +61,7 @@ export const Resource = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['project'] }, - { fields: ['person'] }, - { fields: ['start_date'] }, - ], + indexes: [{ fields: ['project'] }, { fields: ['person'] }, { fields: ['start_date'] }], titleFormat: tmpl`{{record.person.name}} → {{record.project.name}}`, compactLayout: ['person', 'project', 'role', 'allocated_hours_per_week'], diff --git a/packages/project/src/objects/pm_risk.object.ts b/packages/project/src/objects/pm_risk.object.ts index b0b54fb..626ac2c 100644 --- a/packages/project/src/objects/pm_risk.object.ts +++ b/packages/project/src/objects/pm_risk.object.ts @@ -162,11 +162,7 @@ export const Risk = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['project'] }, - { fields: ['status'] }, - { fields: ['category'] }, - ], + indexes: [{ fields: ['project'] }, { fields: ['status'] }, { fields: ['category'] }], titleFormat: tmpl`{{record.name}}`, displayNameField: 'name', @@ -176,7 +172,14 @@ export const Risk = ObjectSchema.create({ type: 'state_machine', name: 'risk_lifecycle', field: 'status', - transitions: {identified:["assessing", "closed"], assessing:["mitigating", "monitoring", "closed"], mitigating:["monitoring", "realized"], monitoring:["mitigating", "closed", "realized"], closed:[], realized:[]}, + transitions: { + identified: ['assessing', 'closed'], + assessing: ['mitigating', 'monitoring', 'closed'], + mitigating: ['monitoring', 'realized'], + monitoring: ['mitigating', 'closed', 'realized'], + closed: [], + realized: [], + }, message: 'Illegal status transition.', }, ], diff --git a/packages/project/src/objects/pm_timesheet.object.ts b/packages/project/src/objects/pm_timesheet.object.ts index af1743e..0366539 100644 --- a/packages/project/src/objects/pm_timesheet.object.ts +++ b/packages/project/src/objects/pm_timesheet.object.ts @@ -14,9 +14,7 @@ export const Timesheet = ObjectSchema.create({ icon: 'clock', description: 'Daily time tracking entry for a project.', - fieldGroups: [ - { key: 'core', label: 'Time Entry', icon: 'clock', defaultExpanded: true }, - ], + fieldGroups: [{ key: 'core', label: 'Time Entry', icon: 'clock', defaultExpanded: true }], fields: { project: Field.lookup('pm_project', { @@ -65,11 +63,7 @@ export const Timesheet = ObjectSchema.create({ trash: true, }, - indexes: [ - { fields: ['project'] }, - { fields: ['person'] }, - { fields: ['work_date'] }, - ], + indexes: [{ fields: ['project'] }, { fields: ['person'] }, { fields: ['work_date'] }], titleFormat: tmpl`{{record.person.name}} - {{record.work_date}}`, compactLayout: ['person', 'project', 'work_date', 'hours'], diff --git a/packages/project/src/translations/en.ts b/packages/project/src/translations/en.ts index 9914c5f..d4727a9 100644 --- a/packages/project/src/translations/en.ts +++ b/packages/project/src/translations/en.ts @@ -15,15 +15,20 @@ export const en: TranslationData = { pm_project: { label: 'Project', pluralLabel: 'Projects', - description: 'A time-bound initiative with milestones, resources, and AI-powered risk prediction.', + description: + 'A time-bound initiative with milestones, resources, and AI-powered risk prediction.', fields: { name: { label: 'Project Name' }, description: { label: 'Description' }, status: { label: 'Status', options: { - planning: 'Planning', active: 'Active', at_risk: 'At Risk', - on_hold: 'On Hold', completed: 'Completed', cancelled: 'Cancelled', + planning: 'Planning', + active: 'Active', + at_risk: 'At Risk', + on_hold: 'On Hold', + completed: 'Completed', + cancelled: 'Cancelled', }, }, priority: { @@ -32,7 +37,12 @@ export const en: TranslationData = { }, project_type: { label: 'Type', - options: { internal: 'Internal', client: 'Client', rnd: 'R&D', maintenance: 'Maintenance' }, + options: { + internal: 'Internal', + client: 'Client', + rnd: 'R&D', + maintenance: 'Maintenance', + }, }, start_date: { label: 'Start Date' }, planned_end_date: { label: 'Planned End Date' }, @@ -63,8 +73,10 @@ export const en: TranslationData = { status: { label: 'Status', options: { - not_started: 'Not Started', in_progress: 'In Progress', - completed: 'Completed', missed: 'Missed', + not_started: 'Not Started', + in_progress: 'In Progress', + completed: 'Completed', + missed: 'Missed', }, }, planned_date: { label: 'Planned Date' }, @@ -85,24 +97,44 @@ export const en: TranslationData = { status: { label: 'Status', options: { - identified: 'Identified', assessing: 'Assessing', mitigating: 'Mitigating', - monitoring: 'Monitoring', closed: 'Closed', realized: 'Realized', + identified: 'Identified', + assessing: 'Assessing', + mitigating: 'Mitigating', + monitoring: 'Monitoring', + closed: 'Closed', + realized: 'Realized', }, }, category: { label: 'Category', options: { - technical: 'Technical', resource: 'Resource', schedule: 'Schedule', - budget: 'Budget', external: 'External', scope: 'Scope', + technical: 'Technical', + resource: 'Resource', + schedule: 'Schedule', + budget: 'Budget', + external: 'External', + scope: 'Scope', }, }, impact: { label: 'Impact (Manual)', - options: { very_low: 'Very Low', low: 'Low', medium: 'Medium', high: 'High', very_high: 'Very High' }, + options: { + very_low: 'Very Low', + low: 'Low', + medium: 'Medium', + high: 'High', + very_high: 'Very High', + }, }, likelihood: { label: 'Likelihood (Manual)', - options: { very_low: 'Very Low', low: 'Low', medium: 'Medium', high: 'High', very_high: 'Very High' }, + options: { + very_low: 'Very Low', + low: 'Low', + medium: 'Medium', + high: 'High', + very_high: 'Very High', + }, }, priority: { label: 'Risk Priority (Impact × Likelihood)' }, ai_impact_score: { label: 'AI Impact Score' }, @@ -126,8 +158,11 @@ export const en: TranslationData = { status: { label: 'Status', options: { - open: 'Open', in_progress: 'In Progress', blocked: 'Blocked', - resolved: 'Resolved', closed: 'Closed', + open: 'Open', + in_progress: 'In Progress', + blocked: 'Blocked', + resolved: 'Resolved', + closed: 'Closed', }, }, severity: { @@ -171,7 +206,8 @@ export const en: TranslationData = { apps: { pm: { label: 'AI Project Management', - description: 'Portfolio tracking with AI risk prediction, delay forecasting, and resource-conflict detection.', + description: + 'Portfolio tracking with AI risk prediction, delay forecasting, and resource-conflict detection.', navigation: { nav_projects: { label: 'Projects' }, nav_milestones: { label: 'Milestones' }, diff --git a/packages/project/src/translations/zh-CN.ts b/packages/project/src/translations/zh-CN.ts index e838f41..b540605 100644 --- a/packages/project/src/translations/zh-CN.ts +++ b/packages/project/src/translations/zh-CN.ts @@ -22,8 +22,12 @@ export const zhCN: TranslationData = { status: { label: '状态', options: { - planning: '规划中', active: '进行中', at_risk: '存在风险', - on_hold: '已暂停', completed: '已完成', cancelled: '已取消', + planning: '规划中', + active: '进行中', + at_risk: '存在风险', + on_hold: '已暂停', + completed: '已完成', + cancelled: '已取消', }, }, priority: { @@ -63,8 +67,10 @@ export const zhCN: TranslationData = { status: { label: '状态', options: { - not_started: '未开始', in_progress: '进行中', - completed: '已完成', missed: '已错过', + not_started: '未开始', + in_progress: '进行中', + completed: '已完成', + missed: '已错过', }, }, planned_date: { label: '计划日期' }, @@ -85,15 +91,23 @@ export const zhCN: TranslationData = { status: { label: '状态', options: { - identified: '已识别', assessing: '评估中', mitigating: '缓解中', - monitoring: '监控中', closed: '已关闭', realized: '已发生', + identified: '已识别', + assessing: '评估中', + mitigating: '缓解中', + monitoring: '监控中', + closed: '已关闭', + realized: '已发生', }, }, category: { label: '类别', options: { - technical: '技术', resource: '资源', schedule: '进度', - budget: '预算', external: '外部', scope: '范围', + technical: '技术', + resource: '资源', + schedule: '进度', + budget: '预算', + external: '外部', + scope: '范围', }, }, impact: { @@ -126,8 +140,11 @@ export const zhCN: TranslationData = { status: { label: '状态', options: { - open: '待处理', in_progress: '处理中', blocked: '受阻', - resolved: '已解决', closed: '已关闭', + open: '待处理', + in_progress: '处理中', + blocked: '受阻', + resolved: '已解决', + closed: '已关闭', }, }, severity: { diff --git a/packages/project/src/views/pm_issue.view.ts b/packages/project/src/views/pm_issue.view.ts index c238e74..c1a2252 100644 --- a/packages/project/src/views/pm_issue.view.ts +++ b/packages/project/src/views/pm_issue.view.ts @@ -23,7 +23,10 @@ export const IssueViews = defineView({ { field: 'reported_at', width: 120, sortable: true }, { field: 'resolved_at', width: 120 }, ], - sort: [{ field: 'priority', order: 'desc' }, { field: 'reported_at', order: 'desc' }], + sort: [ + { field: 'priority', order: 'desc' }, + { field: 'reported_at', order: 'desc' }, + ], grouping: { fields: [{ field: 'status', order: 'asc', collapsed: false }] }, selection: { type: 'multiple' }, pagination: { pageSize: 50, pageSizeOptions: [25, 50, 100] }, @@ -72,7 +75,10 @@ export const IssueViews = defineView({ { field: 'priority', width: 100 }, { field: 'assigned_to', width: 140 }, ], - sort: [{ field: 'status', order: 'asc' }, { field: 'priority', order: 'desc' }], + sort: [ + { field: 'status', order: 'asc' }, + { field: 'priority', order: 'desc' }, + ], }, blocker_issues: { @@ -80,7 +86,10 @@ export const IssueViews = defineView({ type: 'grid', label: 'Blockers', data: { provider: 'object', object: 'pm_issue' }, - filter: [{ field: 'type', operator: 'equals', value: 'blocker' }, { field: 'status', operator: 'equals', value: 'open' }], + filter: [ + { field: 'type', operator: 'equals', value: 'blocker' }, + { field: 'status', operator: 'equals', value: 'open' }, + ], columns: [ { field: 'issue_number', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, diff --git a/packages/project/src/views/pm_milestone.view.ts b/packages/project/src/views/pm_milestone.view.ts index 846050a..40c453f 100644 --- a/packages/project/src/views/pm_milestone.view.ts +++ b/packages/project/src/views/pm_milestone.view.ts @@ -29,10 +29,21 @@ export const MilestoneViews = defineView({ allowedVisualizations: ['grid', 'timeline'], }, tabs: [ - { name: 'all', label: 'All Milestones', view: 'all_milestones', isDefault: true, pinned: true }, + { + name: 'all', + label: 'All Milestones', + view: 'all_milestones', + isDefault: true, + pinned: true, + }, { name: 'upcoming', label: 'Upcoming', icon: 'calendar', view: 'upcoming_milestones' }, { name: 'at_risk', label: 'At Risk', icon: 'alert-triangle', view: 'at_risk_milestones' }, - { name: 'critical_path', label: 'Critical Path', icon: 'zap', view: 'critical_path_milestones' }, + { + name: 'critical_path', + label: 'Critical Path', + icon: 'zap', + view: 'critical_path_milestones', + }, ], }, @@ -42,7 +53,10 @@ export const MilestoneViews = defineView({ type: 'grid', label: 'Upcoming Milestones', data: { provider: 'object', object: 'pm_milestone' }, - filter: [{ field: 'status', operator: 'in', value: ['not_started', 'in_progress'] }, { field: 'due_date', operator: 'not_equals', value: null }], + filter: [ + { field: 'status', operator: 'in', value: ['not_started', 'in_progress'] }, + { field: 'due_date', operator: 'not_equals', value: null }, + ], columns: [ { field: 'name', width: 280, link: true, pinned: 'left' }, { field: 'project', width: 160 }, @@ -82,7 +96,10 @@ export const MilestoneViews = defineView({ { field: 'due_date', width: 120 }, { field: 'completed_at', width: 120 }, ], - sort: [{ field: 'project', order: 'asc' }, { field: 'due_date', order: 'asc' }], + sort: [ + { field: 'project', order: 'asc' }, + { field: 'due_date', order: 'asc' }, + ], }, }, }); diff --git a/packages/project/src/views/pm_project.view.ts b/packages/project/src/views/pm_project.view.ts index 9ffacc3..0b7edb1 100644 --- a/packages/project/src/views/pm_project.view.ts +++ b/packages/project/src/views/pm_project.view.ts @@ -24,7 +24,10 @@ export const ProjectViews = defineView({ { field: 'start_date', width: 120, sortable: true }, { field: 'target_end_date', width: 120, sortable: true }, ], - sort: [{ field: 'priority', order: 'desc' }, { field: 'health', order: 'asc' }], + sort: [ + { field: 'priority', order: 'desc' }, + { field: 'health', order: 'asc' }, + ], grouping: { fields: [{ field: 'status', order: 'asc', collapsed: false }] }, selection: { type: 'multiple' }, pagination: { pageSize: 25, pageSizeOptions: [10, 25, 50] }, diff --git a/packages/project/src/views/pm_risk.view.ts b/packages/project/src/views/pm_risk.view.ts index 9b4c729..0146a72 100644 --- a/packages/project/src/views/pm_risk.view.ts +++ b/packages/project/src/views/pm_risk.view.ts @@ -25,7 +25,10 @@ export const RiskViews = defineView({ { field: 'response_strategy', width: 120 }, { field: 'response_owner', width: 140 }, ], - sort: [{ field: 'priority', order: 'desc' }, { field: 'ai_impact_score', order: 'desc' }], + sort: [ + { field: 'priority', order: 'desc' }, + { field: 'ai_impact_score', order: 'desc' }, + ], grouping: { fields: [{ field: 'status', order: 'asc', collapsed: false }] }, selection: { type: 'multiple' }, pagination: { pageSize: 50, pageSizeOptions: [25, 50, 100] }, @@ -47,7 +50,9 @@ export const RiskViews = defineView({ type: 'grid', label: 'Active Risks', data: { provider: 'object', object: 'pm_risk' }, - filter: [{ field: 'status', operator: 'in', value: ['identified', 'monitoring', 'mitigating'] }], + filter: [ + { field: 'status', operator: 'in', value: ['identified', 'monitoring', 'mitigating'] }, + ], columns: [ { field: 'risk_id', width: 130, link: true, pinned: 'left' }, { field: 'name', width: 280 }, @@ -93,7 +98,10 @@ export const RiskViews = defineView({ { field: 'ai_impact_score', width: 100, align: 'right' }, ], grouping: { fields: [{ field: 'project', order: 'asc', collapsed: true }] }, - sort: [{ field: 'project', order: 'asc' }, { field: 'priority', order: 'desc' }], + sort: [ + { field: 'project', order: 'asc' }, + { field: 'priority', order: 'desc' }, + ], }, }, }); diff --git a/packages/todo/objectstack.manifest.json b/packages/todo/objectstack.manifest.json index affeda4..94b1891 100644 --- a/packages/todo/objectstack.manifest.json +++ b/packages/todo/objectstack.manifest.json @@ -13,7 +13,12 @@ "iconUrl": "https://cdn.objectos.app/icons/todo.svg", "homepageUrl": "https://github.com/objectstack-ai/templates/tree/main/packages/todo", "tags": ["tasks", "projects", "approvals"], - "skills": ["objectstack-platform", "objectstack-data", "objectstack-ui", "objectstack-automation"], + "skills": [ + "objectstack-platform", + "objectstack-data", + "objectstack-ui", + "objectstack-automation" + ], "readmePath": "README.md", "translations": { "zh-CN": { @@ -34,4 +39,3 @@ } } } - diff --git a/packages/todo/src/objects/todo_task.object.ts b/packages/todo/src/objects/todo_task.object.ts index 79d7abf..8bd4e9c 100644 --- a/packages/todo/src/objects/todo_task.object.ts +++ b/packages/todo/src/objects/todo_task.object.ts @@ -103,11 +103,7 @@ export const Task = ObjectSchema.create({ mru: true, }, - indexes: [ - { fields: ['assignee'] }, - { fields: ['status'] }, - { fields: ['due_date'] }, - ], + indexes: [{ fields: ['assignee'] }, { fields: ['status'] }, { fields: ['due_date'] }], titleFormat: tmpl`{{record.subject}}`, compactLayout: ['subject', 'status', 'priority', 'assignee', 'due_date'], @@ -117,7 +113,12 @@ export const Task = ObjectSchema.create({ type: 'state_machine', name: 'task_lifecycle', field: 'status', - transitions: {todo:["doing", "cancelled"], doing:["done", "todo", "cancelled"], done:["todo"], cancelled:[]}, + transitions: { + todo: ['doing', 'cancelled'], + doing: ['done', 'todo', 'cancelled'], + done: ['todo'], + cancelled: [], + }, message: 'Illegal status transition.', }, { diff --git a/packages/todo/src/translations/en.ts b/packages/todo/src/translations/en.ts index ed87ccc..992cf3b 100644 --- a/packages/todo/src/translations/en.ts +++ b/packages/todo/src/translations/en.ts @@ -101,10 +101,19 @@ export const en: TranslationData = { create_task: { label: 'New Task' }, }, widgets: { - my_open_tasks: { title: 'My Open Tasks', description: 'Tasks assigned to you that are still to do or in progress' }, + my_open_tasks: { + title: 'My Open Tasks', + description: 'Tasks assigned to you that are still to do or in progress', + }, my_overdue: { title: 'Overdue', description: 'Your open tasks past their due date' }, - done_this_week: { title: 'Done This Week', description: 'Tasks you completed since the start of the week' }, - recent_overdue_list: { title: 'Overdue Tasks', description: 'Your open tasks past their due date, sorted by oldest first' }, + done_this_week: { + title: 'Done This Week', + description: 'Tasks you completed since the start of the week', + }, + recent_overdue_list: { + title: 'Overdue Tasks', + description: 'Your open tasks past their due date, sorted by oldest first', + }, }, }, }, diff --git a/packages/todo/src/translations/es-ES.ts b/packages/todo/src/translations/es-ES.ts index ad6c17d..1930855 100644 --- a/packages/todo/src/translations/es-ES.ts +++ b/packages/todo/src/translations/es-ES.ts @@ -18,7 +18,12 @@ export const esES: TranslationData = { description: { label: 'Descripción' }, status: { label: 'Estado', - options: { todo: 'Pendiente', doing: 'En curso', done: 'Completada', cancelled: 'Cancelada' }, + options: { + todo: 'Pendiente', + doing: 'En curso', + done: 'Completada', + cancelled: 'Cancelada', + }, }, priority: { label: 'Prioridad', @@ -33,10 +38,19 @@ export const esES: TranslationData = { is_overdue: { label: '¿Vencida?' }, }, _views: { - all_tasks: { label: 'Todas las tareas', description: 'Todas las tareas, agrupadas por estado' }, + all_tasks: { + label: 'Todas las tareas', + description: 'Todas las tareas, agrupadas por estado', + }, task_board: { label: 'Tablero de tareas', description: 'Vista kanban agrupada por estado' }, - my_open_tasks: { label: 'Mis tareas abiertas', description: 'Tareas abiertas asignadas a ti' }, - overdue_tasks: { label: 'Tareas vencidas', description: 'Tareas abiertas pasadas de su fecha límite' }, + my_open_tasks: { + label: 'Mis tareas abiertas', + description: 'Tareas abiertas asignadas a ti', + }, + overdue_tasks: { + label: 'Tareas vencidas', + description: 'Tareas abiertas pasadas de su fecha límite', + }, }, }, @@ -97,10 +111,19 @@ export const esES: TranslationData = { create_task: { label: 'Nueva tarea' }, }, widgets: { - my_open_tasks: { title: 'Mis tareas abiertas', description: 'Tareas asignadas a ti que siguen pendientes o en curso' }, + my_open_tasks: { + title: 'Mis tareas abiertas', + description: 'Tareas asignadas a ti que siguen pendientes o en curso', + }, my_overdue: { title: 'Vencidas', description: 'Tus tareas abiertas pasadas de fecha' }, - done_this_week: { title: 'Completadas esta semana', description: 'Tareas completadas desde el inicio de la semana' }, - recent_overdue_list: { title: 'Tareas vencidas', description: 'Tus tareas vencidas ordenadas de más antigua a más reciente' }, + done_this_week: { + title: 'Completadas esta semana', + description: 'Tareas completadas desde el inicio de la semana', + }, + recent_overdue_list: { + title: 'Tareas vencidas', + description: 'Tus tareas vencidas ordenadas de más antigua a más reciente', + }, }, }, }, diff --git a/packages/todo/src/translations/index.ts b/packages/todo/src/translations/index.ts index 6e52d47..0576011 100644 --- a/packages/todo/src/translations/index.ts +++ b/packages/todo/src/translations/index.ts @@ -23,4 +23,3 @@ export const TodoTranslations: TranslationBundle = { 'ja-JP': jaJP, 'es-ES': esES, }; - diff --git a/packages/todo/src/translations/ja-JP.ts b/packages/todo/src/translations/ja-JP.ts index edaa00b..b9e8fac 100644 --- a/packages/todo/src/translations/ja-JP.ts +++ b/packages/todo/src/translations/ja-JP.ts @@ -33,9 +33,15 @@ export const jaJP: TranslationData = { is_overdue: { label: '期限超過' }, }, _views: { - all_tasks: { label: 'すべてのタスク', description: 'すべてのタスクをステータスでグループ化' }, + all_tasks: { + label: 'すべてのタスク', + description: 'すべてのタスクをステータスでグループ化', + }, task_board: { label: 'タスクボード', description: 'ステータス別のカンバンビュー' }, - my_open_tasks: { label: 'マイ未完了タスク', description: '自分に割り当てられた未完了タスク' }, + my_open_tasks: { + label: 'マイ未完了タスク', + description: '自分に割り当てられた未完了タスク', + }, overdue_tasks: { label: '期限超過タスク', description: '期限を過ぎた未完了タスク' }, }, }, @@ -97,10 +103,16 @@ export const jaJP: TranslationData = { create_task: { label: '新規タスク' }, }, widgets: { - my_open_tasks: { title: 'マイ未完了タスク', description: '自分に割り当てられた未完了または進行中のタスク' }, + my_open_tasks: { + title: 'マイ未完了タスク', + description: '自分に割り当てられた未完了または進行中のタスク', + }, my_overdue: { title: '期限超過', description: '期限を過ぎた自分の未完了タスク' }, done_this_week: { title: '今週の完了', description: '今週の始めから完了したタスク' }, - recent_overdue_list: { title: '期限超過タスク', description: '古い順にソートされた期限超過タスク' }, + recent_overdue_list: { + title: '期限超過タスク', + description: '古い順にソートされた期限超過タスク', + }, }, }, }, diff --git a/scripts/run-qa.mjs b/scripts/run-qa.mjs index 606defe..fc08f55 100644 --- a/scripts/run-qa.mjs +++ b/scripts/run-qa.mjs @@ -24,7 +24,7 @@ function arg(name, fallback) { return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback; } -const baseUrl = (arg('url', 'http://localhost:4002')).replace(/\/$/, ''); +const baseUrl = arg('url', 'http://localhost:4002').replace(/\/$/, ''); const apiBase = `${baseUrl}/api/v1`; const file = arg('file', 'qa/business-workflow.test.json'); @@ -130,7 +130,9 @@ async function execute(action) { case 'query_records': return dataFetch('POST', `${target}/query`, payload); case 'wait': - return new Promise((r) => setTimeout(() => r({ waited: payload.duration || 0 }), payload.duration || 0)); + return new Promise((r) => + setTimeout(() => r({ waited: payload.duration || 0 }), payload.duration || 0), + ); default: throw new Error(`unsupported action type: ${type}`); } @@ -138,7 +140,8 @@ async function execute(action) { async function runStep(step, ctx) { const result = await execute(interpolate(step.action, ctx)); - if (step.capture) for (const [k, p] of Object.entries(step.capture)) ctx[k] = getByPath(result, p); + if (step.capture) + for (const [k, p] of Object.entries(step.capture)) ctx[k] = getByPath(result, p); // Interpolate assertions too so expectedValue can reference captured vars. if (step.assertions) for (const a of step.assertions) assertOne(result, interpolate(a, ctx)); return result; From a678f9d2c17ea354ce671ea92eca2956e59aeb71 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:54:58 +0800 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20fix=20Test=20step=20=E2=80=94=20'--if?= =?UTF-8?q?-present'=20must=20precede=20the=20script=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm -r test --if-present` forwarded `--if-present` to `objectstack build` ("Nonexistent flag"), failing the Test step. Moved the flag before the script: `pnpm -r --if-present test`. (This step was previously masked by the Format check failing first.) --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1901891..f4d8372 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,9 @@ jobs: run: pnpm build - name: Test - run: pnpm -r test --if-present + # `--if-present` must precede the script name, otherwise pnpm forwards it + # to the script (`objectstack build --if-present` → "Nonexistent flag"). + run: pnpm -r --if-present test smoke: name: Smoke (todo dev server)