feat: Dalim DAM web application — full build

Complete client-facing DAM interface for Dalim ES FUSiON GraphQL API:

- Asset browsing, search with faceted filtering, project/folder navigation
- Send To distribution to 17 MMS platforms (PIM, Social, Google, In-Store, Print)
- 10 workflow templates, approval queue, process monitor with progress bars
- Collections with thumbnail mosaics, dashboard with KPI cards and activity feed
- Docker Compose (app + PostgreSQL), mock mode, error boundaries
- Two-tier API reference docs (466 operations indexed, 29 detailed)
- MediaMarkt branding: Noto Sans Display, #DF0000 red, dark sidebar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DJP 2026-04-02 22:11:37 -04:00
parent 92ffb89d29
commit 584b7df68b
98 changed files with 29645 additions and 31 deletions

82
.gitignore vendored
View file

@ -1,50 +1,70 @@
# These are some examples of commonly ignored file patterns.
# You should customize this list as applicable to your project.
# Learn more about .gitignore:
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
# Node artifact files
# Dependencies
dalim-app/node_modules/
dalim-app/.pnp
dalim-app/.pnp.*
node_modules/
dist/
# Compiled Java class files
*.class
# Next.js
dalim-app/.next/
dalim-app/out/
dalim-app/build/
# Compiled Python bytecode
# Generated
dalim-app/src/generated/
# Env files
dalim-app/.env
dalim-app/.env.local
dalim-app/.env.development.local
dalim-app/.env.test.local
dalim-app/.env.production.local
# Large source files (open in browser instead)
FUSION_API_index.html
FUSION_API_index.html.zip
Module9.ESFUSION_GraphQLMassActions.pdf
# Python
__pycache__/
*.pyc
*.py[cod]
venv/
# Log files
# OS
.DS_Store
Thumbs.db
*.pem
# Debug / Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.log
# Package files
*.jar
# Maven
target/
dist/
# JetBrains IDE
# IDE
.idea/
.vscode/
# Unit test reports
TEST*.xml
# Docker volumes
dalim-app/dalim_pgdata/
# Generated by MacOS
.DS_Store
# Claude internal
.claude/
# Generated by Windows
Thumbs.db
# TypeScript
dalim-app/*.tsbuildinfo
dalim-app/next-env.d.ts
# Applications
*.app
*.exe
*.war
# Build artifacts
dist/
target/
*.jar
*.class
# Large media files
# Large media
*.mp4
*.tiff
*.avi
*.flv
*.mov
*.wmv

27
CLAUDE.md Normal file
View file

@ -0,0 +1,27 @@
# DALIM-API Project
## Dalim ES FUSiON API
This project integrates with the Dalim ES FUSiON GraphQL API for digital asset management.
### API Reference Files
- **What can the API do?** → Read `docs/dalim-api-index.md` (all 466 operations, one-line each, grouped by domain)
- **How to call an endpoint?** → Read `docs/dalim-api-reference.md` (detailed docs for active endpoints with args, types, examples)
- **Full browsable docs** → Open `FUSION_API_index.html` in browser (6.6MB SpectaQL page — too large for Claude Code to read directly)
- **Auth & patterns guide**`Module9.ESFUSION_GraphQLMassActions.pdf` (Dalim training presentation)
### API Connection Details
- **GraphQL endpoint:** `https://{HOST}/ES/api/graphql`
- **Token endpoint:** `https://{HOST}/ES/api/oauth/token`
- **Auth method:** OAuth2 with HMAC SHA256 (client_id + client_secret → Bearer token)
### Key Concepts
- **Dependency chain:** Security Profiles → Users → Projects → Assets (must create in this order)
- **API type:** GraphQL (Queries, Mutations, Subscriptions)
- **Pattern:** Use `dalimAPIUtils` helper module with `getHeaders()` and `getResult()` functions
### Adding New Endpoints to Reference
When you need to use a new endpoint not yet in `dalim-api-reference.md`:
1. Find it in `docs/dalim-api-index.md` to confirm it exists
2. Look up its full details in `FUSION_API_index.html` (open in browser)
3. Add the detailed entry to `docs/dalim-api-reference.md`

23
MM_logo_white.svg Normal file
View file

@ -0,0 +1,23 @@
<svg width="288" height="39" viewBox="0 0 288 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_309_1614)">
<path d="M109.506 12.5383L110.705 5.90368H105.056L103.857 12.5383H109.506Z" fill="white"/>
<path d="M147.759 22.5867C150.34 23.8262 153.754 24.1107 155.735 21.8348C155.379 22.0685 154.343 22.7289 152.86 22.8508C150.543 23.0337 148.359 21.7332 146.845 20.0669C143.482 16.3686 141.948 10.1608 143.38 3.65829C141.501 4.99943 139.936 6.63522 138.686 8.46405C139.387 14.5703 142.506 20.0771 147.759 22.5867Z" fill="white"/>
<path d="M152.931 17.5066C153.185 17.1611 153.926 16.1959 155.288 15.566C157.401 14.6008 159.901 15.0783 161.892 16.1248C166.322 18.4515 169.888 23.7653 170.762 30.3592C172.062 28.4592 172.987 26.3866 173.536 24.2428C170.782 18.7461 165.976 14.6313 160.175 14.0725C157.32 13.788 154.018 14.7024 152.931 17.5066Z" fill="white"/>
<path d="M153.947 21.9263C153.52 21.8755 152.311 21.7231 151.092 20.8493C149.192 19.5081 148.369 17.1002 148.277 14.8548C148.074 9.85599 150.889 4.1155 156.172 0.051431C153.926 -0.111132 151.661 0.11239 149.486 0.711839C146.103 5.84272 144.945 12.0709 147.363 17.3643C148.531 19.9857 150.97 22.3936 153.947 21.9263Z" fill="white"/>
<path d="M155.451 11.8271C153.083 13.4425 151.132 16.2569 152.108 19.1119C152.077 18.6852 152.027 17.4659 152.667 16.1146C153.662 14.0115 155.887 12.7821 158.082 12.3046C162.969 11.2378 169.116 13.0158 174.023 17.5066C173.8 15.2815 173.2 13.0869 172.215 11.0447C166.586 8.58597 160.256 8.52501 155.451 11.8271Z" fill="white"/>
<path d="M150.391 13.1276C149.619 15.881 149.934 19.2948 152.514 20.8493C152.22 20.5445 151.397 19.6402 151.01 18.1873C150.421 15.9419 151.336 13.5644 152.707 11.7864C155.765 7.83412 161.618 5.24327 168.262 5.5176C166.657 3.95293 164.788 2.66259 162.725 1.72786C156.842 3.48556 151.945 7.50899 150.391 13.1276Z" fill="white"/>
<path d="M240.542 13.219L240.227 14.9564C238.571 13.727 236.508 13.026 234.232 13.0463C228.715 13.0971 223.493 17.2424 221.268 22.5867L225.22 0.864244H218.525L200.816 24.3749L205.083 0.874403H199.281L167.257 37.9793H175.303L197.331 12.457L192.657 37.9793H197.961L217.173 12.4875L212.54 37.9793H218.474L220.14 28.8047C220.394 34.3928 224.143 38.7007 229.589 38.6397C231.926 38.6194 234.232 37.8777 236.295 36.6382L236.051 37.9692H241.7L246.201 13.2088H240.542V13.219ZM239.048 26.0106C238.164 30.2271 234.466 33.1126 230.676 33.1126C227.283 33.1126 224.865 29.8613 225.596 26.2138C226.44 21.9974 229.772 18.8274 233.765 18.8274C237.616 18.8274 239.892 21.9771 239.048 26.0106Z" fill="white"/>
<path d="M154.587 16.8157C155.003 16.7141 156.192 16.4499 157.635 16.8462C159.88 17.466 161.475 19.437 162.329 21.5199C164.229 26.1529 163.548 32.5132 159.972 38.1317C162.116 37.5323 164.168 36.5569 166.027 35.2259C167.45 29.2416 166.413 22.9931 162.329 18.8477C160.338 16.7852 157.218 15.3526 154.587 16.8157Z" fill="white"/>
<path d="M282.696 13.219L284.027 5.90368H278.378L277.047 13.219H268.198L262.467 20.2803L265.079 5.90368H259.43L258.149 12.9447H257.479C256.107 12.9447 254.35 13.473 252.704 14.4382L252.927 13.2088H247.278L242.777 37.9692H248.426L251.007 23.7551C251.901 19.7723 254.258 18.3194 256.869 18.3194H257.164L253.598 37.959H259.247L261.116 27.6566L267.304 37.959H278.195L281.731 18.4921H287.035L288 13.1987H282.696V13.219ZM272.912 35.9676L265.932 24.3545L273.776 14.7329L273.085 18.5124H276.082L272.912 35.9676Z" fill="white"/>
<path d="M98.3504 5.91385L96.6739 15.1291C94.9874 13.788 92.8436 13.0158 90.4762 13.0463C84.6748 13.1072 79.2086 17.7301 77.2376 23.5316C77.2782 17.4863 73.1532 13.0463 67.3619 13.0463C61.9364 13.0463 56.5616 16.8665 54.1333 21.9059L57.9536 0.874402H51.258L33.559 24.3749L37.8263 0.874402H32.0248L0 37.9793H8.04685L30.0741 12.457L25.4004 37.9793H30.704L49.9067 12.4875L45.2737 37.9793H51.2072L52.8227 29.0689C53.3307 34.4944 57.1814 38.6499 62.8406 38.6499C68.4084 38.6499 73.4986 34.85 75.8558 29.6073H69.8816C68.4185 31.7918 66.163 33.1329 63.5823 33.1329C61.1845 33.1329 58.5835 31.4362 58.3498 28.2662H76.4349C76.4654 34.1083 80.2551 38.7109 85.8432 38.6499C88.2715 38.6194 90.6591 37.8269 92.7826 36.4858L92.5083 37.9895H98.1573L103.979 5.924H98.3504V5.91385ZM59.9856 22.6984C61.5096 20.3718 63.8769 18.8477 66.4678 18.8477C69.1704 18.8477 70.8976 20.3921 71.4158 22.6984H59.9856ZM95.221 26.0208C94.3371 30.2373 90.8014 33.1228 87.0015 33.1228C84.1363 33.1228 80.9054 30.7046 81.8706 26.224C82.7748 22.0177 86.1886 18.8376 89.9377 18.8376C93.7884 18.8376 96.0643 21.9872 95.221 26.0208Z" fill="white"/>
<path d="M160.927 23.8669C160.713 21.0118 159.25 17.913 156.294 17.3339C156.68 17.5269 157.767 18.0857 158.61 19.3151C159.931 21.2252 159.89 23.7652 159.2 25.9192C157.676 30.6843 153.063 35.1243 146.713 37.1259C148.796 38.0708 151 38.6194 153.225 38.782C158.173 35.1243 161.384 29.6785 160.927 23.8669Z" fill="white"/>
<path d="M156.619 26.8235C158.285 24.4968 159.159 21.1846 157.269 18.8376C157.442 19.2338 157.909 20.3616 157.767 21.845C157.554 24.1615 155.887 26.0818 153.977 27.2807C149.751 29.9528 143.36 30.3897 137.203 27.8395C138.158 29.8309 139.469 31.68 141.125 33.2955C147.272 33.6714 153.236 31.5683 156.619 26.8235Z" fill="white"/>
<path d="M126.88 14.9564C125.224 13.727 123.161 13.026 120.886 13.0463C114.922 13.1072 109.313 17.9232 107.444 23.8973L109.384 13.219H103.735L99.2343 37.9793H104.883L106.793 27.4534C106.458 33.6816 110.36 38.7109 116.232 38.6499C118.569 38.6296 120.875 37.8879 122.938 36.6483L122.694 37.9793H128.343L132.844 13.219H127.195L126.88 14.9564ZM125.702 26.0106C124.818 30.2271 121.119 33.1126 117.33 33.1126C113.936 33.1126 111.518 29.8613 112.249 26.2138C113.093 21.9974 116.425 18.8274 120.418 18.8274C124.269 18.8274 126.545 21.9771 125.702 26.0106Z" fill="white"/>
<path d="M151.417 26.3155C154.191 25.6144 156.985 23.623 157.046 20.6156C156.924 21.0322 156.558 22.1904 155.491 23.2471C153.835 24.8829 151.325 25.2791 149.09 24.9743C144.132 24.3037 138.961 20.5343 135.882 14.6313C135.323 16.836 135.161 19.1017 135.384 21.337C139.855 25.5534 145.778 27.7785 151.417 26.3155Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_309_1614">
<rect width="288" height="38.8085" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6 KiB

6
dalim-app/.dockerignore Normal file
View file

@ -0,0 +1,6 @@
node_modules
.next
.env
.env.local
*.md
.git

19
dalim-app/.env.example Normal file
View file

@ -0,0 +1,19 @@
# Dalim ES FUSiON API
DALIM_HOST=your-server.es-cloud.com
DALIM_PROTOCOL=https
DALIM_GRAPHQL_URL=https://your-server.es-cloud.com/ES/api/graphql
DALIM_TOKEN_URL=https://your-server.es-cloud.com/ES/api/oauth/token
DALIM_CLIENT_ID=your_client_id
DALIM_CLIENT_SECRET=your_client_secret
DALIM_USERNAME=admin
DALIM_PASSWORD=your_password
# Use mock data instead of real API (set to "true" when no API access)
DALIM_MOCK_MODE=true
# PostgreSQL
DATABASE_URL=postgresql://dalim:dalim_secret@localhost:5490/dalim_app
# Next.js
NEXTAUTH_SECRET=change-me-to-random-string
NEXTAUTH_URL=http://localhost:3100

48
dalim-app/.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
!.env.example
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
/src/generated/prisma

5
dalim-app/AGENTS.md Normal file
View file

@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

1
dalim-app/CLAUDE.md Normal file
View file

@ -0,0 +1 @@
@AGENTS.md

36
dalim-app/Dockerfile Normal file
View file

@ -0,0 +1,36 @@
FROM node:20-alpine AS base
# Dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholder"
RUN npx prisma generate
RUN npm run build
# Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

151
dalim-app/README.md Normal file
View file

@ -0,0 +1,151 @@
# Dalim DAM — Digital Asset Management
A client-facing web application that surfaces the **Dalim ES FUSiON** Digital Asset Management system via its GraphQL API. Built for MediaMarkt/MMS to manage, distribute, and track creative assets across 17+ output platforms.
## Features
### Asset Management
- **Asset browsing** with thumbnail previews, file type badges, and status indicators
- **Asset detail pages** with full metadata, preview, download button, and distribution history
- **Project-based navigation** with folder tree sidebar and filtered asset grids
- **Full-text search** with interactive facet filtering (file type, status), list/grid toggle
### Distribution & Workflows
- **Send To** dialog — distribute assets to 17 connected platforms from the MMS tech architecture:
- **Web & App**: Website, Mobile App, E-Commerce (Shop/PWA)
- **Social**: Meta (Facebook/Instagram), TikTok, LinkedIn
- **Google**: Google Ads, Performance Max, CM360, YouTube
- **In-Store**: Electronic Shelf Labels (Pricer), In-Store TV, Digital Screens, POP
- **Print**: Print Fulfillment (flyers, catalogues, large-format)
- **Advertising**: Customer Comms Hub, Programmatic (DV360/Trade Desk)
- **Product Data**: PIM (Stibo STEP)
- **10 workflow templates** including Print Approval, Digital Review, Packaging QC, and platform-specific distribution workflows
- **Approval queue** with status tracking and approver assignment
- **Process monitor** with real-time progress bars, status dots, and timestamps
- **Distribution history** on every asset showing channel, status, and initiator
### Collections & Dashboard
- **Collections** with thumbnail mosaics and asset counts
- **Dashboard** with 5 KPI cards, recent assets grid, project list, and activity feed combining approvals + distributions
- **Process Monitor page** with stats, progress bars, and linked assets
### Infrastructure
- **Docker Compose** — containerized app + PostgreSQL
- **Mock mode** — full UI development without API access
- **Error boundary** — graceful error handling with retry
- **Custom 404 page**
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Framework | Next.js (App Router, standalone output) |
| UI | Tailwind CSS, shadcn/ui, Noto Sans Display |
| State | TanStack React Query |
| API | Next.js API routes (proxy to Dalim GraphQL) |
| Database | PostgreSQL 16 + Prisma 7 |
| Auth | OAuth2 + HMAC SHA256 (Dalim ES FUSiON) |
| Container | Docker + Docker Compose |
| Branding | MediaMarkt red (#DF0000), dark sidebar (#1A1A1A) |
## Quick Start
### Prerequisites
- Node.js 20+
- Docker & Docker Compose
### Development (local)
```bash
cp .env.example .env
npm install
npx prisma generate
npm run dev -- -p 3100
```
App runs at `http://localhost:3100` in mock mode.
### Docker
```bash
cp .env.example .env
docker compose up --build -d
```
- App: `http://localhost:3100`
- PostgreSQL: `localhost:5490`
### Environment Variables
See `.env.example` for all required variables. Key settings:
| Variable | Description |
|----------|-------------|
| `DALIM_MOCK_MODE` | Set to `true` for mock data (no API needed) |
| `DALIM_GRAPHQL_URL` | Dalim ES FUSiON GraphQL endpoint |
| `DALIM_CLIENT_ID` | OAuth2 client ID |
| `DALIM_CLIENT_SECRET` | OAuth2 client secret |
| `DATABASE_URL` | PostgreSQL connection string |
## Project Structure
```
src/
app/
api/ # API routes (proxy to Dalim)
assets/ # Asset CRUD + by-ID
channels/ # Distribution channels
distribution/ # Send-to jobs (GET/POST)
folders/ # Folder by-ID + assets
projects/ # Projects + nested folders/assets
processes/ # Process monitor
search/ # Full-text search
workflows/ # Workflow templates
approvals/ # Approval queue
collections/ # Asset collections
assets/ # Asset pages (browse + detail)
collections/ # Collections page
processes/ # Process monitor page
projects/ # Projects (browse + detail with folder tree)
search/ # Search with facets
workflows/ # Workflows & distribution dashboard
components/
assets/ # AssetCard, AssetGrid, AssetDetail
distribution/ # SendToDialog
folders/ # FolderTree
layout/ # Sidebar, Header
ui/ # shadcn/ui components
hooks/
use-dalim.ts # React Query hooks for all API endpoints
lib/
dalim-client.ts # GraphQL client with OAuth2 token caching
dalim-service.ts # Unified service layer (mock/real)
dalim-queries.ts # GraphQL query strings
dalim-types.ts # TypeScript interfaces
mock/data.ts # Mock data (projects, assets, channels, jobs)
```
## API Reference
- **Full capability index**: See `docs/dalim-api-index.md` (466 operations, one-line each)
- **Detailed endpoint docs**: See `docs/dalim-api-reference.md` (29 active endpoints with args, types, examples)
- **Browsable docs**: Open `FUSION_API_index.html` in browser (6.6MB SpectaQL page)
## Architecture
```
Browser → Next.js App → API Routes → Dalim ES FUSiON GraphQL API
PostgreSQL (sessions, preferences)
```
- Dalim API credentials stay server-side (API routes act as proxy)
- React Query handles client-side caching and state
- Mock mode returns static data from `lib/mock/data.ts`
## Ports
| Service | Port |
|---------|------|
| App | 3100 |
| PostgreSQL | 5490 |

25
dalim-app/components.json Normal file
View file

@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

View file

@ -0,0 +1,32 @@
services:
app:
build: .
ports:
- "3100:3000"
environment:
- DATABASE_URL=postgresql://dalim:dalim_secret@db:5432/dalim_app
- DALIM_MOCK_MODE=true
env_file:
- .env
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
ports:
- "5490:5432"
environment:
POSTGRES_USER: dalim
POSTGRES_PASSWORD: dalim_secret
POSTGRES_DB: dalim_app
volumes:
- dalim_pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dalim -d dalim_app"]
interval: 5s
timeout: 5s
retries: 5
volumes:
dalim_pgdata:

View file

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
dalim-app/next.config.ts Normal file
View file

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;

10760
dalim-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

37
dalim-app/package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "dalim-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@prisma/client": "^7.6.0",
"@tanstack/react-query": "^5.96.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"graphql-request": "^7.4.0",
"lucide-react": "^1.7.0",
"next": "16.2.2",
"prisma": "^7.6.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"shadcn": "^4.1.2",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.2",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View file

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View file

@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

View file

@ -0,0 +1,27 @@
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model Session {
id String @id @default(cuid())
dalimUserId String?
dalimLogin String?
accessToken String?
tokenExpiry DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model UserPreference {
id String @id @default(cuid())
userId String @unique
viewMode String @default("grid")
pageSize Int @default(24)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View file

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View file

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1 KiB

23
dalim-app/public/logo.svg Normal file
View file

@ -0,0 +1,23 @@
<svg width="288" height="39" viewBox="0 0 288 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_309_1614)">
<path d="M109.506 12.5383L110.705 5.90368H105.056L103.857 12.5383H109.506Z" fill="white"/>
<path d="M147.759 22.5867C150.34 23.8262 153.754 24.1107 155.735 21.8348C155.379 22.0685 154.343 22.7289 152.86 22.8508C150.543 23.0337 148.359 21.7332 146.845 20.0669C143.482 16.3686 141.948 10.1608 143.38 3.65829C141.501 4.99943 139.936 6.63522 138.686 8.46405C139.387 14.5703 142.506 20.0771 147.759 22.5867Z" fill="white"/>
<path d="M152.931 17.5066C153.185 17.1611 153.926 16.1959 155.288 15.566C157.401 14.6008 159.901 15.0783 161.892 16.1248C166.322 18.4515 169.888 23.7653 170.762 30.3592C172.062 28.4592 172.987 26.3866 173.536 24.2428C170.782 18.7461 165.976 14.6313 160.175 14.0725C157.32 13.788 154.018 14.7024 152.931 17.5066Z" fill="white"/>
<path d="M153.947 21.9263C153.52 21.8755 152.311 21.7231 151.092 20.8493C149.192 19.5081 148.369 17.1002 148.277 14.8548C148.074 9.85599 150.889 4.1155 156.172 0.051431C153.926 -0.111132 151.661 0.11239 149.486 0.711839C146.103 5.84272 144.945 12.0709 147.363 17.3643C148.531 19.9857 150.97 22.3936 153.947 21.9263Z" fill="white"/>
<path d="M155.451 11.8271C153.083 13.4425 151.132 16.2569 152.108 19.1119C152.077 18.6852 152.027 17.4659 152.667 16.1146C153.662 14.0115 155.887 12.7821 158.082 12.3046C162.969 11.2378 169.116 13.0158 174.023 17.5066C173.8 15.2815 173.2 13.0869 172.215 11.0447C166.586 8.58597 160.256 8.52501 155.451 11.8271Z" fill="white"/>
<path d="M150.391 13.1276C149.619 15.881 149.934 19.2948 152.514 20.8493C152.22 20.5445 151.397 19.6402 151.01 18.1873C150.421 15.9419 151.336 13.5644 152.707 11.7864C155.765 7.83412 161.618 5.24327 168.262 5.5176C166.657 3.95293 164.788 2.66259 162.725 1.72786C156.842 3.48556 151.945 7.50899 150.391 13.1276Z" fill="white"/>
<path d="M240.542 13.219L240.227 14.9564C238.571 13.727 236.508 13.026 234.232 13.0463C228.715 13.0971 223.493 17.2424 221.268 22.5867L225.22 0.864244H218.525L200.816 24.3749L205.083 0.874403H199.281L167.257 37.9793H175.303L197.331 12.457L192.657 37.9793H197.961L217.173 12.4875L212.54 37.9793H218.474L220.14 28.8047C220.394 34.3928 224.143 38.7007 229.589 38.6397C231.926 38.6194 234.232 37.8777 236.295 36.6382L236.051 37.9692H241.7L246.201 13.2088H240.542V13.219ZM239.048 26.0106C238.164 30.2271 234.466 33.1126 230.676 33.1126C227.283 33.1126 224.865 29.8613 225.596 26.2138C226.44 21.9974 229.772 18.8274 233.765 18.8274C237.616 18.8274 239.892 21.9771 239.048 26.0106Z" fill="white"/>
<path d="M154.587 16.8157C155.003 16.7141 156.192 16.4499 157.635 16.8462C159.88 17.466 161.475 19.437 162.329 21.5199C164.229 26.1529 163.548 32.5132 159.972 38.1317C162.116 37.5323 164.168 36.5569 166.027 35.2259C167.45 29.2416 166.413 22.9931 162.329 18.8477C160.338 16.7852 157.218 15.3526 154.587 16.8157Z" fill="white"/>
<path d="M282.696 13.219L284.027 5.90368H278.378L277.047 13.219H268.198L262.467 20.2803L265.079 5.90368H259.43L258.149 12.9447H257.479C256.107 12.9447 254.35 13.473 252.704 14.4382L252.927 13.2088H247.278L242.777 37.9692H248.426L251.007 23.7551C251.901 19.7723 254.258 18.3194 256.869 18.3194H257.164L253.598 37.959H259.247L261.116 27.6566L267.304 37.959H278.195L281.731 18.4921H287.035L288 13.1987H282.696V13.219ZM272.912 35.9676L265.932 24.3545L273.776 14.7329L273.085 18.5124H276.082L272.912 35.9676Z" fill="white"/>
<path d="M98.3504 5.91385L96.6739 15.1291C94.9874 13.788 92.8436 13.0158 90.4762 13.0463C84.6748 13.1072 79.2086 17.7301 77.2376 23.5316C77.2782 17.4863 73.1532 13.0463 67.3619 13.0463C61.9364 13.0463 56.5616 16.8665 54.1333 21.9059L57.9536 0.874402H51.258L33.559 24.3749L37.8263 0.874402H32.0248L0 37.9793H8.04685L30.0741 12.457L25.4004 37.9793H30.704L49.9067 12.4875L45.2737 37.9793H51.2072L52.8227 29.0689C53.3307 34.4944 57.1814 38.6499 62.8406 38.6499C68.4084 38.6499 73.4986 34.85 75.8558 29.6073H69.8816C68.4185 31.7918 66.163 33.1329 63.5823 33.1329C61.1845 33.1329 58.5835 31.4362 58.3498 28.2662H76.4349C76.4654 34.1083 80.2551 38.7109 85.8432 38.6499C88.2715 38.6194 90.6591 37.8269 92.7826 36.4858L92.5083 37.9895H98.1573L103.979 5.924H98.3504V5.91385ZM59.9856 22.6984C61.5096 20.3718 63.8769 18.8477 66.4678 18.8477C69.1704 18.8477 70.8976 20.3921 71.4158 22.6984H59.9856ZM95.221 26.0208C94.3371 30.2373 90.8014 33.1228 87.0015 33.1228C84.1363 33.1228 80.9054 30.7046 81.8706 26.224C82.7748 22.0177 86.1886 18.8376 89.9377 18.8376C93.7884 18.8376 96.0643 21.9872 95.221 26.0208Z" fill="white"/>
<path d="M160.927 23.8669C160.713 21.0118 159.25 17.913 156.294 17.3339C156.68 17.5269 157.767 18.0857 158.61 19.3151C159.931 21.2252 159.89 23.7652 159.2 25.9192C157.676 30.6843 153.063 35.1243 146.713 37.1259C148.796 38.0708 151 38.6194 153.225 38.782C158.173 35.1243 161.384 29.6785 160.927 23.8669Z" fill="white"/>
<path d="M156.619 26.8235C158.285 24.4968 159.159 21.1846 157.269 18.8376C157.442 19.2338 157.909 20.3616 157.767 21.845C157.554 24.1615 155.887 26.0818 153.977 27.2807C149.751 29.9528 143.36 30.3897 137.203 27.8395C138.158 29.8309 139.469 31.68 141.125 33.2955C147.272 33.6714 153.236 31.5683 156.619 26.8235Z" fill="white"/>
<path d="M126.88 14.9564C125.224 13.727 123.161 13.026 120.886 13.0463C114.922 13.1072 109.313 17.9232 107.444 23.8973L109.384 13.219H103.735L99.2343 37.9793H104.883L106.793 27.4534C106.458 33.6816 110.36 38.7109 116.232 38.6499C118.569 38.6296 120.875 37.8879 122.938 36.6483L122.694 37.9793H128.343L132.844 13.219H127.195L126.88 14.9564ZM125.702 26.0106C124.818 30.2271 121.119 33.1126 117.33 33.1126C113.936 33.1126 111.518 29.8613 112.249 26.2138C113.093 21.9974 116.425 18.8274 120.418 18.8274C124.269 18.8274 126.545 21.9771 125.702 26.0106Z" fill="white"/>
<path d="M151.417 26.3155C154.191 25.6144 156.985 23.623 157.046 20.6156C156.924 21.0322 156.558 22.1904 155.491 23.2471C153.835 24.8829 151.325 25.2791 149.09 24.9743C144.132 24.3037 138.961 20.5343 135.882 14.6313C135.323 16.836 135.161 19.1017 135.384 21.337C139.855 25.5534 145.778 27.7785 151.417 26.3155Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_309_1614">
<rect width="288" height="38.8085" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View file

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View file

@ -0,0 +1,16 @@
import { NextRequest } from "next/server";
import { getApprovals } from "@/lib/dalim-service";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get("limit") ?? "20");
const cursor = searchParams.get("cursor") ?? undefined;
try {
const data = await getApprovals(limit, cursor);
return Response.json(data);
} catch (error) {
console.error("Failed to fetch approvals:", error);
return Response.json({ error: "Failed to fetch approvals" }, { status: 500 });
}
}

View file

@ -0,0 +1,19 @@
import { getAssetById } from "@/lib/dalim-service";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const asset = await getAssetById(id);
if (!asset) {
return Response.json({ error: "Asset not found" }, { status: 404 });
}
return Response.json(asset);
} catch (error) {
console.error("Failed to fetch asset:", error);
return Response.json({ error: "Failed to fetch asset" }, { status: 500 });
}
}

View file

@ -0,0 +1,16 @@
import { NextRequest } from "next/server";
import { getAssets } from "@/lib/dalim-service";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get("limit") ?? "24");
const cursor = searchParams.get("cursor") ?? undefined;
try {
const data = await getAssets(limit, cursor);
return Response.json(data);
} catch (error) {
console.error("Failed to fetch assets:", error);
return Response.json({ error: "Failed to fetch assets" }, { status: 500 });
}
}

View file

@ -0,0 +1,11 @@
import { getChannels } from "@/lib/dalim-service";
export async function GET() {
try {
const channels = await getChannels();
return Response.json(channels);
} catch (error) {
console.error("Failed to fetch channels:", error);
return Response.json({ error: "Failed to fetch channels" }, { status: 500 });
}
}

View file

@ -0,0 +1,16 @@
import { NextRequest } from "next/server";
import { getCollections } from "@/lib/dalim-service";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get("limit") ?? "20");
const cursor = searchParams.get("cursor") ?? undefined;
try {
const data = await getCollections(limit, cursor);
return Response.json(data);
} catch (error) {
console.error("Failed to fetch collections:", error);
return Response.json({ error: "Failed to fetch collections" }, { status: 500 });
}
}

View file

@ -0,0 +1,34 @@
import { getDistributionJobs, sendToChannel } from "@/lib/dalim-service";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const assetId = request.nextUrl.searchParams.get("assetId") ?? undefined;
try {
const jobs = await getDistributionJobs(assetId);
return Response.json(jobs);
} catch (error) {
console.error("Failed to fetch distribution jobs:", error);
return Response.json({ error: "Failed to fetch jobs" }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { assetId, channelId } = body;
if (!assetId || !channelId) {
return Response.json(
{ error: "assetId and channelId are required" },
{ status: 400 }
);
}
const job = await sendToChannel(assetId, channelId);
return Response.json(job, { status: 201 });
} catch (error) {
console.error("Failed to create distribution job:", error);
return Response.json({ error: "Failed to send to channel" }, { status: 500 });
}
}

View file

@ -0,0 +1,20 @@
import { getAssetsByFolder } from "@/lib/dalim-service";
import { NextRequest } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get("limit") ?? "24", 10);
const cursor = searchParams.get("cursor") ?? undefined;
try {
const assets = await getAssetsByFolder(id, limit, cursor);
return Response.json(assets);
} catch (error) {
console.error("Failed to fetch folder assets:", error);
return Response.json({ error: "Failed to fetch assets" }, { status: 500 });
}
}

View file

@ -0,0 +1,19 @@
import { getFolderById } from "@/lib/dalim-service";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const folder = await getFolderById(id);
if (!folder) {
return Response.json({ error: "Folder not found" }, { status: 404 });
}
return Response.json(folder);
} catch (error) {
console.error("Failed to fetch folder:", error);
return Response.json({ error: "Failed to fetch folder" }, { status: 500 });
}
}

View file

@ -0,0 +1,9 @@
import { isMockMode } from "@/lib/dalim-client";
export async function GET() {
return Response.json({
status: "ok",
mockMode: isMockMode(),
timestamp: new Date().toISOString(),
});
}

View file

@ -0,0 +1,21 @@
import { getProcesses } from "@/lib/dalim-service";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const limit = parseInt(
request.nextUrl.searchParams.get("limit") ?? "20",
10
);
const cursor = request.nextUrl.searchParams.get("cursor") ?? undefined;
try {
const data = await getProcesses(limit, cursor);
return Response.json(data);
} catch (error) {
console.error("Failed to fetch processes:", error);
return Response.json(
{ error: "Failed to fetch processes" },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,21 @@
import { getAssetsByProject } from "@/lib/dalim-service";
import { NextRequest } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const searchParams = request.nextUrl.searchParams;
const folderId = searchParams.get("folderId") ?? undefined;
const limit = parseInt(searchParams.get("limit") ?? "24", 10);
const cursor = searchParams.get("cursor") ?? undefined;
try {
const assets = await getAssetsByProject(id, folderId, limit, cursor);
return Response.json(assets);
} catch (error) {
console.error("Failed to fetch project assets:", error);
return Response.json({ error: "Failed to fetch assets" }, { status: 500 });
}
}

View file

@ -0,0 +1,16 @@
import { getFoldersByProject } from "@/lib/dalim-service";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const folders = await getFoldersByProject(id);
return Response.json(folders);
} catch (error) {
console.error("Failed to fetch project folders:", error);
return Response.json({ error: "Failed to fetch folders" }, { status: 500 });
}
}

View file

@ -0,0 +1,19 @@
import { getProjectById } from "@/lib/dalim-service";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
try {
const project = await getProjectById(id);
if (!project) {
return Response.json({ error: "Project not found" }, { status: 404 });
}
return Response.json(project);
} catch (error) {
console.error("Failed to fetch project:", error);
return Response.json({ error: "Failed to fetch project" }, { status: 500 });
}
}

View file

@ -0,0 +1,16 @@
import { NextRequest } from "next/server";
import { getProjects } from "@/lib/dalim-service";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get("limit") ?? "20");
const cursor = searchParams.get("cursor") ?? undefined;
try {
const data = await getProjects(limit, cursor);
return Response.json(data);
} catch (error) {
console.error("Failed to fetch projects:", error);
return Response.json({ error: "Failed to fetch projects" }, { status: 500 });
}
}

View file

@ -0,0 +1,17 @@
import { NextRequest } from "next/server";
import { searchAssets } from "@/lib/dalim-service";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q") ?? "";
const limit = parseInt(searchParams.get("limit") ?? "24");
const cursor = searchParams.get("cursor") ?? undefined;
try {
const data = await searchAssets(query, [], limit, cursor);
return Response.json(data);
} catch (error) {
console.error("Search failed:", error);
return Response.json({ error: "Search failed" }, { status: 500 });
}
}

View file

@ -0,0 +1,16 @@
import { NextRequest } from "next/server";
import { getWorkflows } from "@/lib/dalim-service";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get("limit") ?? "20");
const cursor = searchParams.get("cursor") ?? undefined;
try {
const data = await getWorkflows(limit, cursor);
return Response.json(data);
} catch (error) {
console.error("Failed to fetch workflows:", error);
return Response.json({ error: "Failed to fetch workflows" }, { status: 500 });
}
}

View file

@ -0,0 +1,226 @@
"use client";
import { useState } from "react";
import { useParams } from "next/navigation";
import { useAsset, useDistributionJobs, useProcesses } from "@/hooks/use-dalim";
import { AssetDetail } from "@/components/assets/asset-detail";
import { SendToDialog } from "@/components/distribution/send-to-dialog";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import Link from "next/link";
function getJobStatusColor(status: string) {
switch (status) {
case "completed": return "bg-green-100 text-green-800";
case "processing": return "bg-blue-100 text-blue-800";
case "queued": return "bg-yellow-100 text-yellow-800";
case "failed": return "bg-red-100 text-red-800";
default: return "bg-gray-100 text-gray-800";
}
}
export default function AssetDetailPage() {
const { id } = useParams<{ id: string }>();
const { data: asset, isLoading } = useAsset(id);
const { data: jobs } = useDistributionJobs(id);
const { data: processes } = useProcesses();
const [sendToOpen, setSendToOpen] = useState(false);
// Filter processes for this asset
const assetProcesses =
processes?.items.filter((p) => p.entity?.id === id) ?? [];
if (isLoading) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-64" />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Skeleton className="lg:col-span-2 aspect-[16/10]" />
<Skeleton className="h-96" />
</div>
</div>
);
}
if (!asset) {
return (
<div className="text-center py-12">
<h2 className="text-xl font-semibold">Asset not found</h2>
<Link
href="/assets"
className="text-primary hover:underline mt-2 inline-block"
>
Back to assets
</Link>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link
href="/assets"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
</Link>
<h2 className="text-2xl font-bold tracking-tight">{asset.name}</h2>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
// Mock download — in real app would hit downloadAsset API
const link = document.createElement("a");
link.href = `/placeholders/placeholder-${((parseInt(id.replace(/\D/g, ""), 10) || 1) - 1) % 8 + 1}.jpg`;
link.download = asset.name;
link.click();
}}
className="flex items-center gap-2 px-4 py-2 border border-border rounded-md hover:bg-muted transition-colors text-sm font-medium"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
Download
</button>
<button
onClick={() => setSendToOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm font-medium"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
/>
</svg>
Send To
</button>
</div>
</div>
<AssetDetail asset={asset} />
{/* Distribution history */}
{jobs && jobs.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3">Distribution History</h3>
<div className="space-y-2">
{jobs.map((job) => (
<Card
key={job.id}
className="p-3 flex items-center justify-between"
>
<div>
<p className="text-sm font-medium">{job.channelName}</p>
<p className="text-xs text-muted-foreground">
by {job.initiatedBy} &middot;{" "}
{new Date(job.initiatedAt).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
<Badge className={getJobStatusColor(job.status)}>
{job.status}
</Badge>
</Card>
))}
</div>
</div>
)}
{/* Process Monitor */}
{assetProcesses.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3">Processes</h3>
<div className="space-y-2">
{assetProcesses.map((proc) => (
<Card key={proc.id} className="p-4">
<div className="flex items-center justify-between mb-2">
<p className="text-sm font-medium">{proc.name}</p>
<Badge
className={
proc.status === "COMPLETED"
? "bg-green-100 text-green-800"
: proc.status === "IN_PROGRESS"
? "bg-blue-100 text-blue-800"
: "bg-yellow-100 text-yellow-800"
}
>
{proc.status}
</Badge>
</div>
{proc.progress !== undefined && (
<div className="w-full bg-muted rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-500"
style={{ width: `${proc.progress}%` }}
/>
</div>
)}
{proc.startDate && (
<p className="text-xs text-muted-foreground mt-2">
Started{" "}
{new Date(proc.startDate).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
})}
{proc.endDate &&
` — Completed ${new Date(proc.endDate).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
})}`}
</p>
)}
</Card>
))}
</div>
</div>
)}
<SendToDialog
assetId={asset.id}
assetName={asset.name}
isOpen={sendToOpen}
onClose={() => setSendToOpen(false)}
/>
</div>
);
}

View file

@ -0,0 +1,27 @@
"use client";
import { useAssets } from "@/hooks/use-dalim";
import { AssetGrid } from "@/components/assets/asset-grid";
export default function AssetsPage() {
const { data, isLoading } = useAssets(24);
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Assets</h2>
<p className="text-muted-foreground">
Browse all assets across projects
</p>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{data?.totalCount ?? 0} assets
</p>
</div>
<AssetGrid assets={data?.items} isLoading={isLoading} />
</div>
);
}

View file

@ -0,0 +1,116 @@
"use client";
import { useCollections } from "@/hooks/use-dalim";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import Image from "next/image";
// Mock: assign some assets to collections for visual appeal
const collectionAssets: Record<string, string[]> = {
COL001: ["A001", "A002", "A005"],
COL002: ["A007", "A003"],
COL003: ["A004", "A006", "A008", "A003"],
};
function getPlaceholderIndex(id: string): number {
const num = parseInt(id.replace(/\D/g, ""), 10) || 1;
return ((num - 1) % 8) + 1;
}
export default function CollectionsPage() {
const { data, isLoading } = useCollections();
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Collections</h2>
<p className="text-muted-foreground">
Browse curated asset collections
</p>
</div>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i} className="overflow-hidden">
<Skeleton className="h-40 w-full" />
<div className="p-4 space-y-2">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-full" />
</div>
</Card>
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data?.items.map((collection) => {
const assetIds = collectionAssets[collection.id] ?? [];
return (
<Card
key={collection.id}
className="overflow-hidden hover:shadow-md transition-shadow cursor-pointer group"
>
{/* Thumbnail mosaic */}
<div className="h-40 bg-muted relative overflow-hidden">
{assetIds.length >= 3 ? (
<div className="grid grid-cols-3 h-full">
{assetIds.slice(0, 3).map((aId, i) => (
<div key={aId} className="relative overflow-hidden">
<Image
src={`/placeholders/placeholder-${getPlaceholderIndex(aId)}.jpg`}
alt=""
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="33vw"
/>
{i < 2 && (
<div className="absolute right-0 top-0 bottom-0 w-px bg-white/30" />
)}
</div>
))}
</div>
) : assetIds.length > 0 ? (
<Image
src={`/placeholders/placeholder-${getPlaceholderIndex(assetIds[0])}.jpg`}
alt=""
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 33vw"
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground/30">
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6.878V6a2.25 2.25 0 012.25-2.25h7.5A2.25 2.25 0 0118 6v.878m-12 0c.235-.083.487-.128.75-.128h10.5c.263 0 .515.045.75.128m-12 0A2.25 2.25 0 004.5 9v.878m13.5-3A2.25 2.25 0 0119.5 9v.878m0 0a2.246 2.246 0 00-.75-.128H5.25c-.263 0-.515.045-.75.128m15 0A2.25 2.25 0 0121 12v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6c0-.98.626-1.813 1.5-2.122" />
</svg>
</div>
)}
</div>
<div className="p-4">
<div className="flex items-center justify-between">
<p className="font-medium">{collection.name}</p>
<Badge variant="outline" className="text-xs">
{assetIds.length} assets
</Badge>
</div>
{collection.description && (
<p className="text-sm text-muted-foreground mt-1">
{collection.description}
</p>
)}
{collection.creationDate && (
<p className="text-xs text-muted-foreground mt-3">
Created{" "}
{new Date(collection.creationDate).toLocaleDateString()}
</p>
)}
</div>
</Card>
);
})}
</div>
)}
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,131 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: "Noto Sans Display", system-ui, sans-serif;
--font-mono: var(--font-geist-mono);
--font-heading: "Noto Sans Display", system-ui, sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
/* MediaMarkt-inspired color scheme — red #DF0000 */
:root {
--background: #ffffff;
--foreground: #1a1a1a;
--card: #ffffff;
--card-foreground: #1a1a1a;
--popover: #ffffff;
--popover-foreground: #1a1a1a;
--primary: #df0000;
--primary-foreground: #ffffff;
--secondary: #f5f5f5;
--secondary-foreground: #1a1a1a;
--muted: #f5f5f5;
--muted-foreground: #737373;
--accent: #fef2f2;
--accent-foreground: #df0000;
--destructive: #b91c1c;
--border: #e5e5e5;
--input: #e5e5e5;
--ring: #df0000;
--chart-1: #df0000;
--chart-2: #333333;
--chart-3: #737373;
--chart-4: #a3a3a3;
--chart-5: #d4d4d4;
--radius: 0.5rem;
--sidebar: #1a1a1a;
--sidebar-foreground: #f5f5f5;
--sidebar-primary: #df0000;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #333333;
--sidebar-accent-foreground: #f5f5f5;
--sidebar-border: #333333;
--sidebar-ring: #df0000;
}
.dark {
--background: #0a0a0a;
--foreground: #f5f5f5;
--card: #1a1a1a;
--card-foreground: #f5f5f5;
--popover: #1a1a1a;
--popover-foreground: #f5f5f5;
--primary: #df0000;
--primary-foreground: #ffffff;
--secondary: #262626;
--secondary-foreground: #f5f5f5;
--muted: #262626;
--muted-foreground: #a3a3a3;
--accent: #371111;
--accent-foreground: #fca5a5;
--destructive: #ef4444;
--border: #333333;
--input: #333333;
--ring: #df0000;
--chart-1: #df0000;
--chart-2: #a3a3a3;
--chart-3: #737373;
--chart-4: #525252;
--chart-5: #333333;
--sidebar: #0a0a0a;
--sidebar-foreground: #f5f5f5;
--sidebar-primary: #df0000;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #1a1a1a;
--sidebar-accent-foreground: #f5f5f5;
--sidebar-border: #333333;
--sidebar-ring: #df0000;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}

View file

@ -0,0 +1,46 @@
import type { Metadata } from "next";
import { Noto_Sans_Display } from "next/font/google";
import "./globals.css";
import { Providers } from "@/components/providers";
import { Sidebar } from "@/components/layout/sidebar";
import { Header } from "@/components/layout/header";
import { ErrorBoundary } from "@/components/error-boundary";
const notoSansDisplay = Noto_Sans_Display({
variable: "--font-noto-sans-display",
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "Dalim DAM",
description: "Digital Asset Management powered by Dalim ES FUSiON",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${notoSansDisplay.variable} h-full antialiased`}
style={{ fontFamily: "'Noto Sans Display', system-ui, sans-serif" }}
>
<body className="min-h-full">
<Providers>
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<Header />
<main className="flex-1 overflow-auto p-6">
<ErrorBoundary>{children}</ErrorBoundary>
</main>
</div>
</div>
</Providers>
</body>
</html>
);
}

View file

@ -0,0 +1,19 @@
import Link from "next/link";
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center">
<p className="text-8xl font-bold text-primary/20">404</p>
<h2 className="text-xl font-semibold mt-4">Page not found</h2>
<p className="text-muted-foreground mt-2">
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<Link
href="/"
className="mt-6 px-6 py-2.5 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm font-medium"
>
Back to Dashboard
</Link>
</div>
);
}

218
dalim-app/src/app/page.tsx Normal file
View file

@ -0,0 +1,218 @@
"use client";
import {
useProjects,
useAssets,
useApprovals,
useDistributionJobs,
useChannels,
} from "@/hooks/use-dalim";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { AssetGrid } from "@/components/assets/asset-grid";
import Image from "next/image";
import Link from "next/link";
function getPlaceholderIndex(id: string): number {
const num = parseInt(id.replace(/\D/g, ""), 10) || 1;
return ((num - 1) % 8) + 1;
}
function getJobStatusColor(status: string) {
switch (status) {
case "completed":
return "bg-green-100 text-green-800";
case "processing":
return "bg-blue-100 text-blue-800";
case "queued":
return "bg-yellow-100 text-yellow-800";
case "failed":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
}
export default function DashboardPage() {
const { data: projects } = useProjects(5);
const { data: assets } = useAssets(8);
const { data: approvals } = useApprovals(5);
const { data: jobs } = useDistributionJobs();
const { data: channels } = useChannels();
const pendingApprovals =
approvals?.items.filter((a) => a.status === "PENDING") ?? [];
const connectedChannels =
channels?.filter((c) => c.status === "connected").length ?? 0;
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">
Overview of your digital asset management
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<Card className="p-5">
<p className="text-sm text-muted-foreground">Projects</p>
<p className="text-3xl font-bold mt-1">
{projects?.totalCount ?? "—"}
</p>
</Card>
<Card className="p-5">
<p className="text-sm text-muted-foreground">Assets</p>
<p className="text-3xl font-bold mt-1">
{assets?.totalCount ?? "—"}
</p>
</Card>
<Card className="p-5">
<p className="text-sm text-muted-foreground">Pending Approvals</p>
<p className="text-3xl font-bold mt-1 text-yellow-600">
{pendingApprovals.length || "—"}
</p>
</Card>
<Card className="p-5">
<p className="text-sm text-muted-foreground">Distributions</p>
<p className="text-3xl font-bold mt-1">{jobs?.length ?? "—"}</p>
</Card>
<Card className="p-5">
<p className="text-sm text-muted-foreground">Platforms</p>
<p className="text-3xl font-bold mt-1 text-green-600">
{connectedChannels || "—"}
</p>
</Card>
</div>
{/* Recent Assets */}
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Recent Assets</h3>
<Link
href="/assets"
className="text-sm text-primary hover:underline"
>
View all
</Link>
</div>
<AssetGrid
assets={assets?.items.slice(0, 5)}
isLoading={false}
linkPrefix="/assets"
/>
</div>
<Separator />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Recent Projects */}
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Recent Projects</h3>
<Link
href="/projects"
className="text-sm text-primary hover:underline"
>
View all
</Link>
</div>
<div className="space-y-2">
{projects?.items.map((project) => (
<Link key={project.id} href={`/projects/${project.id}`}>
<Card className="p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div className="min-w-0">
<p className="font-medium text-sm">{project.name}</p>
<p className="text-xs text-muted-foreground mt-0.5 truncate">
{project.description}
</p>
</div>
<div className="flex items-center gap-2 shrink-0 ml-3">
{project.status && (
<Badge variant="outline" className="text-xs">
{project.status}
</Badge>
)}
</div>
</div>
</Card>
</Link>
))}
</div>
</div>
{/* Activity feed: approvals + distributions */}
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Activity</h3>
<Link
href="/workflows"
className="text-sm text-primary hover:underline"
>
View all
</Link>
</div>
<div className="space-y-2">
{/* Pending approvals */}
{pendingApprovals.map((approval) => (
<Card
key={approval.id}
className="p-3 flex items-center justify-between"
>
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 relative rounded overflow-hidden bg-muted shrink-0">
<Image
src={`/placeholders/placeholder-${getPlaceholderIndex(approval.entity.id)}.jpg`}
alt=""
fill
className="object-cover"
sizes="32px"
/>
</div>
<div className="min-w-0">
<p className="text-sm font-medium truncate">
{approval.entity.name}
</p>
<p className="text-xs text-muted-foreground">
Approval needed &middot;{" "}
{approval.approver.firstName}{" "}
{approval.approver.lastName}
</p>
</div>
</div>
<Badge className="bg-yellow-100 text-yellow-800 shrink-0">
Pending
</Badge>
</Card>
))}
{/* Recent distributions */}
{jobs?.slice(0, 4).map((job) => (
<Card
key={job.id}
className="p-3 flex items-center justify-between"
>
<div className="min-w-0">
<p className="text-sm font-medium truncate">
{job.assetName}
</p>
<p className="text-xs text-muted-foreground">
Sent to {job.channelName}
</p>
</div>
<Badge
className={`shrink-0 ${getJobStatusColor(job.status)}`}
>
{job.status}
</Badge>
</Card>
))}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,158 @@
"use client";
import { useProcesses } from "@/hooks/use-dalim";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import Link from "next/link";
function getStatusColor(status: string) {
switch (status) {
case "COMPLETED":
return "bg-green-100 text-green-800";
case "IN_PROGRESS":
return "bg-blue-100 text-blue-800";
case "QUEUED":
return "bg-yellow-100 text-yellow-800";
case "FAILED":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
}
export default function ProcessesPage() {
const { data, isLoading } = useProcesses();
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Process Monitor</h2>
<p className="text-muted-foreground">
Track running, queued, and completed processes
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4">
<p className="text-sm text-muted-foreground">Total</p>
<p className="text-2xl font-bold mt-1">
{data?.totalCount ?? "—"}
</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">In Progress</p>
<p className="text-2xl font-bold mt-1 text-blue-600">
{data?.items.filter((p) => p.status === "IN_PROGRESS").length ??
"—"}
</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">Queued</p>
<p className="text-2xl font-bold mt-1 text-yellow-600">
{data?.items.filter((p) => p.status === "QUEUED").length ?? "—"}
</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">Completed</p>
<p className="text-2xl font-bold mt-1 text-green-600">
{data?.items.filter((p) => p.status === "COMPLETED").length ??
"—"}
</p>
</Card>
</div>
{/* Process list */}
{isLoading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
) : (
<div className="space-y-3">
{data?.items.map((process) => (
<Card key={process.id} className="p-5">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div
className={`w-3 h-3 rounded-full ${
process.status === "IN_PROGRESS"
? "bg-blue-500 animate-pulse"
: process.status === "COMPLETED"
? "bg-green-500"
: process.status === "QUEUED"
? "bg-yellow-500"
: "bg-red-500"
}`}
/>
<div>
<p className="font-medium">{process.name}</p>
{process.entity && (
<Link
href={`/assets/${process.entity.id}`}
className="text-xs text-primary hover:underline"
>
{process.entity.name}
</Link>
)}
</div>
</div>
<Badge className={getStatusColor(process.status)}>
{process.status.replace("_", " ")}
</Badge>
</div>
{/* Progress bar */}
{process.progress !== undefined && (
<div className="mb-2">
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
<span>Progress</span>
<span>{process.progress}%</span>
</div>
<div className="w-full bg-muted rounded-full h-2.5">
<div
className={`h-2.5 rounded-full transition-all duration-700 ${
process.progress === 100
? "bg-green-500"
: "bg-primary"
}`}
style={{ width: `${process.progress}%` }}
/>
</div>
</div>
)}
{/* Timestamps */}
<div className="flex gap-4 text-xs text-muted-foreground mt-2">
{process.startDate && (
<span>
Started:{" "}
{new Date(process.startDate).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
})}
</span>
)}
{process.endDate && (
<span>
Completed:{" "}
{new Date(process.endDate).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
})}
</span>
)}
</div>
</Card>
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,157 @@
"use client";
import { useState } from "react";
import { useParams } from "next/navigation";
import {
useProject,
useProjectFolders,
useProjectAssets,
} from "@/hooks/use-dalim";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import { FolderTree } from "@/components/folders/folder-tree";
import { AssetGrid } from "@/components/assets/asset-grid";
import Link from "next/link";
export default function ProjectDetailPage() {
const { id } = useParams<{ id: string }>();
const [selectedFolderId, setSelectedFolderId] = useState<string | undefined>(
undefined
);
const { data: project, isLoading: projectLoading } = useProject(id);
const { data: folders, isLoading: foldersLoading } = useProjectFolders(id);
const { data: assets, isLoading: assetsLoading } = useProjectAssets(
id,
selectedFolderId
);
if (projectLoading) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-96" />
<div className="grid grid-cols-4 gap-6">
<Skeleton className="h-64" />
<div className="col-span-3">
<Skeleton className="h-64" />
</div>
</div>
</div>
);
}
if (!project) {
return (
<div className="text-center py-12">
<h2 className="text-xl font-semibold">Project not found</h2>
<Link href="/projects" className="text-primary hover:underline mt-2 inline-block">
Back to projects
</Link>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-3">
<Link
href="/projects"
className="text-muted-foreground hover:text-foreground transition-colors"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
</Link>
<h2 className="text-2xl font-bold tracking-tight">
{project.name}
</h2>
{project.status && (
<Badge variant="outline">{project.status}</Badge>
)}
</div>
{project.description && (
<p className="text-muted-foreground mt-1 ml-8">
{project.description}
</p>
)}
</div>
{project.customer && (
<span className="text-sm text-muted-foreground">
{project.customer.name}
</span>
)}
</div>
{/* Stats bar */}
<div className="flex gap-6 text-sm">
<div>
<span className="text-muted-foreground">Folders: </span>
<span className="font-medium">{folders?.length ?? 0}</span>
</div>
<div>
<span className="text-muted-foreground">Assets: </span>
<span className="font-medium">{assets?.totalCount ?? 0}</span>
</div>
{project.lastModificationDate && (
<div>
<span className="text-muted-foreground">Updated: </span>
<span className="font-medium">
{new Date(project.lastModificationDate).toLocaleDateString()}
</span>
</div>
)}
</div>
<Separator />
{/* Folder tree + asset grid */}
<div className="grid grid-cols-1 lg:grid-cols-[240px_1fr] gap-6">
<Card className="p-2 h-fit">
<h3 className="px-3 py-2 text-sm font-medium text-muted-foreground uppercase tracking-wide">
Folders
</h3>
<FolderTree
folders={folders}
isLoading={foldersLoading}
selectedId={selectedFolderId}
onSelect={setSelectedFolderId}
/>
</Card>
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{selectedFolderId
? `Showing assets in "${folders?.find((f) => f.id === selectedFolderId)?.name}"`
: "All project assets"}
</p>
<p className="text-sm text-muted-foreground">
{assets?.totalCount ?? 0} assets
</p>
</div>
<AssetGrid
assets={assets?.items}
isLoading={assetsLoading}
linkPrefix="/assets"
/>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,64 @@
"use client";
import { useProjects } from "@/hooks/use-dalim";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import Link from "next/link";
export default function ProjectsPage() {
const { data, isLoading } = useProjects(20);
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Projects</h2>
<p className="text-muted-foreground">
Browse all projects and their assets
</p>
</div>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i} className="p-4 space-y-3">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-1/2" />
</Card>
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data?.items.map((project) => (
<Link key={project.id} href={`/projects/${project.id}`}>
<Card className="p-4 hover:shadow-md transition-shadow h-full">
<p className="font-medium">{project.name}</p>
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{project.description}
</p>
<div className="flex items-center gap-2 mt-4">
{project.status && (
<Badge variant="outline" className="text-xs">
{project.status}
</Badge>
)}
{project.customer && (
<span className="text-xs text-muted-foreground">
{project.customer.name}
</span>
)}
</div>
{project.lastModificationDate && (
<p className="text-xs text-muted-foreground mt-2">
Updated {new Date(project.lastModificationDate).toLocaleDateString()}
</p>
)}
</Card>
</Link>
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,275 @@
"use client";
import { useState, Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useSearch } from "@/hooks/use-dalim";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import Image from "next/image";
import Link from "next/link";
function getPlaceholderIndex(id: string): number {
const num = parseInt(id.replace(/\D/g, ""), 10) || 1;
return ((num - 1) % 8) + 1;
}
function SearchContent() {
const searchParams = useSearchParams();
const router = useRouter();
const initialQuery = searchParams.get("q") ?? "";
const [query, setQuery] = useState(initialQuery);
const [activeFacets, setActiveFacets] = useState<Record<string, string>>({});
const [viewMode, setViewMode] = useState<"grid" | "list">("list");
const { data, isLoading } = useSearch(initialQuery);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) {
router.push(`/search?q=${encodeURIComponent(query.trim())}`);
}
};
const toggleFacet = (name: string, value: string) => {
setActiveFacets((prev) => {
const next = { ...prev };
if (next[name] === value) {
delete next[name];
} else {
next[name] = value;
}
return next;
});
};
// Client-side facet filtering on mock data
const filteredItems = data?.items.filter((item) => {
for (const [facetName, facetValue] of Object.entries(activeFacets)) {
if (facetName === "mimeType" && item.mimeType !== facetValue) return false;
// For status facet we don't have it on SearchResult, skip
}
return true;
});
return (
<div className="space-y-6">
{/* Search bar */}
<form onSubmit={handleSubmit} className="flex gap-3">
<Input
type="search"
placeholder="Search assets, projects, files..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1 text-base h-12"
autoFocus
/>
<button
type="submit"
className="px-6 h-12 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors font-medium"
>
Search
</button>
</form>
{!initialQuery ? (
<div className="text-center py-16">
<svg
className="w-16 h-16 mx-auto text-muted-foreground/30"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
<h3 className="text-lg font-medium mt-4 text-muted-foreground">
Search across all assets
</h3>
<p className="text-sm text-muted-foreground mt-1">
Try &ldquo;banner&rdquo;, &ldquo;packaging&rdquo;, or &ldquo;logo&rdquo;
</p>
</div>
) : isLoading ? (
<div className="space-y-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-20 w-full" />
))}
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-[220px_1fr] gap-6">
{/* Facet sidebar */}
{data?.facets && data.facets.length > 0 && (
<div className="space-y-5">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold">Filters</p>
{Object.keys(activeFacets).length > 0 && (
<button
onClick={() => setActiveFacets({})}
className="text-xs text-primary hover:underline"
>
Clear all
</button>
)}
</div>
{data.facets.map((facet) => (
<div key={facet.name}>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
{facet.name === "mimeType" ? "File Type" : facet.name}
</p>
<div className="space-y-1">
{facet.values.map((v) => {
const isActive = activeFacets[facet.name] === v.value;
return (
<button
key={v.value}
onClick={() => toggleFacet(facet.name, v.value)}
className={`flex items-center justify-between w-full px-2 py-1.5 rounded text-sm transition-colors ${
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted"
}`}
>
<span className="truncate">
{v.value.split("/").pop()}
</span>
<span className="text-xs ml-2 shrink-0">{v.count}</span>
</button>
);
})}
</div>
</div>
))}
</div>
)}
{/* Results */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{filteredItems?.length ?? 0} results for &ldquo;{initialQuery}&rdquo;
{Object.keys(activeFacets).length > 0 && " (filtered)"}
</p>
<div className="flex gap-1">
<button
onClick={() => setViewMode("list")}
className={`p-1.5 rounded ${viewMode === "list" ? "bg-muted" : "hover:bg-muted"}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" />
</svg>
</button>
<button
onClick={() => setViewMode("grid")}
className={`p-1.5 rounded ${viewMode === "grid" ? "bg-muted" : "hover:bg-muted"}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
</button>
</div>
</div>
{viewMode === "list" ? (
<div className="space-y-2">
{filteredItems?.map((item) => (
<Link key={item.id} href={`/assets/${item.id}`}>
<Card className="p-4 flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="w-16 h-12 relative rounded overflow-hidden bg-muted shrink-0">
<Image
src={`/placeholders/placeholder-${getPlaceholderIndex(item.id)}.jpg`}
alt={item.name}
fill
className="object-cover"
sizes="64px"
/>
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{item.name}
</p>
{item.description && (
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">
{item.description}
</p>
)}
</div>
<div className="flex gap-2 shrink-0">
{item.mimeType && (
<Badge variant="secondary" className="text-xs">
{item.mimeType.split("/").pop()}
</Badge>
)}
</div>
</Card>
</Link>
))}
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredItems?.map((item) => (
<Link key={item.id} href={`/assets/${item.id}`}>
<Card className="overflow-hidden hover:shadow-md transition-shadow cursor-pointer group">
<div className="aspect-[4/3] relative bg-muted overflow-hidden">
<Image
src={`/placeholders/placeholder-${getPlaceholderIndex(item.id)}.jpg`}
alt={item.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 50vw, 25vw"
/>
</div>
<div className="p-3">
<p className="text-sm font-medium truncate">{item.name}</p>
{item.mimeType && (
<p className="text-xs text-muted-foreground mt-1">
{item.mimeType.split("/").pop()}
</p>
)}
</div>
</Card>
</Link>
))}
</div>
)}
{filteredItems?.length === 0 && (
<div className="text-center py-12 text-muted-foreground">
No results match your filters
</div>
)}
</div>
</div>
)}
</div>
);
}
export default function SearchPage() {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Search</h2>
<p className="text-muted-foreground">
Find assets across all projects
</p>
</div>
<Suspense
fallback={
<div className="space-y-4">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-64 w-full" />
</div>
}
>
<SearchContent />
</Suspense>
</div>
);
}

View file

@ -0,0 +1,293 @@
"use client";
import {
useWorkflows,
useApprovals,
useChannels,
useDistributionJobs,
} from "@/hooks/use-dalim";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
function getApprovalStatusColor(status: string): string {
switch (status) {
case "APPROVED":
return "bg-green-100 text-green-800";
case "PENDING":
return "bg-yellow-100 text-yellow-800";
case "REJECTED":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
}
function getJobStatusColor(status: string): string {
switch (status) {
case "completed":
return "bg-green-100 text-green-800";
case "processing":
return "bg-blue-100 text-blue-800";
case "queued":
return "bg-yellow-100 text-yellow-800";
case "failed":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
}
function getChannelStatusColor(status: string): string {
switch (status) {
case "connected":
return "bg-green-500";
case "available":
return "bg-yellow-500";
case "coming_soon":
return "bg-gray-400";
default:
return "bg-gray-400";
}
}
const categoryLabels: Record<string, string> = {
pim: "Product Data",
social: "Social Media",
web: "Web & App",
instore: "In-Store",
print: "Print",
advertising: "Advertising",
google: "Google",
};
export default function WorkflowsPage() {
const { data: workflows, isLoading: wfLoading } = useWorkflows();
const { data: approvals, isLoading: apLoading } = useApprovals();
const { data: channels, isLoading: chLoading } = useChannels();
const { data: jobs, isLoading: jobsLoading } = useDistributionJobs();
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">
Workflows & Distribution
</h2>
<p className="text-muted-foreground">
Manage workflows, approvals, and asset distribution to platforms
</p>
</div>
{/* Stats row */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4">
<p className="text-sm text-muted-foreground">Workflows</p>
<p className="text-2xl font-bold mt-1">
{workflows?.items.length ?? "—"}
</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">Pending Approvals</p>
<p className="text-2xl font-bold mt-1">
{approvals?.items.filter((a) => a.status === "PENDING").length ??
"—"}
</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">
Connected Platforms
</p>
<p className="text-2xl font-bold mt-1">
{channels?.filter((c) => c.status === "connected").length ?? "—"}
</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">
Recent Distributions
</p>
<p className="text-2xl font-bold mt-1">{jobs?.length ?? "—"}</p>
</Card>
</div>
{/* Two-column layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left: Workflows & Approvals */}
<div className="space-y-8">
{/* Active Workflows */}
<div>
<h3 className="text-lg font-semibold mb-4">Active Workflows</h3>
{wfLoading ? (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
) : (
<div className="space-y-2">
{workflows?.items.map((wf) => (
<Card key={wf.id} className="p-4">
<div className="flex items-start justify-between">
<div>
<p className="font-medium text-sm">{wf.name}</p>
{wf.description && (
<p className="text-xs text-muted-foreground mt-1">
{wf.description}
</p>
)}
</div>
<Badge variant="outline" className="text-xs shrink-0">
v{wf.revision}
</Badge>
</div>
</Card>
))}
</div>
)}
</div>
<Separator />
{/* Approval Queue */}
<div>
<h3 className="text-lg font-semibold mb-4">Approval Queue</h3>
{apLoading ? (
<div className="space-y-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
) : (
<div className="space-y-2">
{approvals?.items.map((approval) => (
<Card
key={approval.id}
className="p-4 flex items-center justify-between"
>
<div>
<p className="font-medium text-sm">
{approval.entity.name}
</p>
<p className="text-xs text-muted-foreground">
{approval.entity.type} &middot; Level{" "}
{approval.level} &middot;{" "}
{approval.approver.firstName}{" "}
{approval.approver.lastName}
</p>
</div>
<Badge className={getApprovalStatusColor(approval.status)}>
{approval.status}
</Badge>
</Card>
))}
</div>
)}
</div>
</div>
{/* Right: Distribution channels & activity */}
<div className="space-y-8">
{/* Connected Platforms */}
<div>
<h3 className="text-lg font-semibold mb-4">
Distribution Platforms
</h3>
{chLoading ? (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
) : (
<div className="space-y-4">
{Object.entries(
channels?.reduce(
(acc, ch) => {
if (!acc[ch.category]) acc[ch.category] = [];
acc[ch.category].push(ch);
return acc;
},
{} as Record<string, typeof channels>
) ?? {}
).map(([category, items]) => (
<div key={category}>
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
{categoryLabels[category] ?? category}
</p>
<div className="space-y-1">
{items!.map((ch) => (
<div
key={ch.id}
className="flex items-center justify-between py-2 px-3 rounded-md hover:bg-muted transition-colors"
>
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${getChannelStatusColor(ch.status)}`}
/>
<span className="text-sm">{ch.name}</span>
</div>
<span className="text-xs text-muted-foreground capitalize">
{ch.status.replace("_", " ")}
</span>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
<Separator />
{/* Recent Distribution Activity */}
<div>
<h3 className="text-lg font-semibold mb-4">
Recent Distribution Activity
</h3>
{jobsLoading ? (
<div className="space-y-2">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-14 w-full" />
))}
</div>
) : (
<div className="space-y-2">
{jobs?.map((job) => (
<Card
key={job.id}
className="p-3 flex items-center justify-between"
>
<div className="min-w-0">
<p className="text-sm font-medium truncate">
{job.assetName}
</p>
<p className="text-xs text-muted-foreground">
&rarr; {job.channelName} &middot; by{" "}
{job.initiatedBy} &middot;{" "}
{new Date(job.initiatedAt).toLocaleDateString(
"en-GB",
{
day: "numeric",
month: "short",
hour: "2-digit",
minute: "2-digit",
}
)}
</p>
</div>
<Badge
className={`shrink-0 ${getJobStatusColor(job.status)}`}
>
{job.status}
</Badge>
</Card>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,88 @@
"use client";
import type { Asset } from "@/lib/dalim-types";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import Image from "next/image";
function formatFileSize(bytes?: number): string {
if (!bytes) return "—";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
function getFileIcon(mimeType?: string): string {
if (!mimeType) return "DOC";
if (mimeType.includes("pdf")) return "PDF";
if (mimeType.includes("image") || mimeType.includes("tiff")) return "IMG";
if (mimeType.includes("illustrator") || mimeType.includes("svg")) return "AI";
if (mimeType.includes("indesign")) return "INDD";
if (mimeType.includes("zip")) return "ZIP";
return "FILE";
}
function getStatusColor(status?: string): string {
switch (status) {
case "APPROVED":
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
case "IN_REVIEW":
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
case "PENDING":
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
case "REJECTED":
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300";
default:
return "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300";
}
}
function getPlaceholderIndex(id: string): number {
const num = parseInt(id.replace(/\D/g, ""), 10) || 1;
return ((num - 1) % 8) + 1;
}
export function AssetCard({ asset }: { asset: Asset }) {
const icon = getFileIcon(asset.mimeType);
const placeholderSrc = `/placeholders/placeholder-${getPlaceholderIndex(asset.id)}.jpg`;
return (
<Card className="group cursor-pointer hover:shadow-md transition-shadow overflow-hidden">
<div className="aspect-[4/3] bg-muted relative rounded-t-lg overflow-hidden">
<Image
src={placeholderSrc}
alt={asset.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 20vw"
/>
<div className="absolute top-2 left-2">
<span className="text-xs font-bold bg-black/60 text-white px-2 py-0.5 rounded">
{icon}
</span>
</div>
</div>
<div className="p-3">
<p className="text-sm font-medium truncate" title={asset.name}>
{asset.name}
</p>
<div className="flex items-center justify-between mt-2">
<span className="text-xs text-muted-foreground">
{formatFileSize(asset.fileSize)}
</span>
{asset.status && (
<Badge variant="secondary" className={`text-xs ${getStatusColor(asset.status)}`}>
{asset.status}
</Badge>
)}
</div>
{asset.project && (
<p className="text-xs text-muted-foreground mt-1 truncate">
{asset.project.name}
</p>
)}
</div>
</Card>
);
}

View file

@ -0,0 +1,178 @@
"use client";
import type { Asset } from "@/lib/dalim-types";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import Image from "next/image";
import Link from "next/link";
function formatFileSize(bytes?: number): string {
if (!bytes) return "—";
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
function getFileIcon(mimeType?: string): string {
if (!mimeType) return "DOC";
if (mimeType.includes("pdf")) return "PDF";
if (mimeType.includes("image") || mimeType.includes("tiff")) return "IMG";
if (mimeType.includes("illustrator") || mimeType.includes("svg")) return "AI";
if (mimeType.includes("indesign")) return "INDD";
if (mimeType.includes("zip")) return "ZIP";
return "FILE";
}
function getStatusColor(status?: string): string {
switch (status) {
case "APPROVED":
return "bg-green-100 text-green-800";
case "IN_REVIEW":
return "bg-yellow-100 text-yellow-800";
case "PENDING":
return "bg-blue-100 text-blue-800";
case "REJECTED":
return "bg-red-100 text-red-800";
default:
return "bg-gray-100 text-gray-800";
}
}
function formatDate(dateStr?: string): string {
if (!dateStr) return "—";
return new Date(dateStr).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function getPlaceholderIndex(id: string): number {
const num = parseInt(id.replace(/\D/g, ""), 10) || 1;
return ((num - 1) % 8) + 1;
}
export function AssetDetail({ asset }: { asset: Asset }) {
const icon = getFileIcon(asset.mimeType);
const placeholderSrc = `/placeholders/placeholder-${getPlaceholderIndex(asset.id)}.jpg`;
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Preview area */}
<div className="lg:col-span-2">
<Card className="aspect-[16/10] relative bg-muted overflow-hidden">
<Image
src={placeholderSrc}
alt={asset.name}
fill
className="object-cover"
sizes="(max-width: 1024px) 100vw, 66vw"
priority
/>
<div className="absolute top-3 left-3">
<span className="text-sm font-bold bg-black/60 text-white px-3 py-1 rounded">
{icon}
</span>
</div>
</Card>
</div>
{/* Metadata panel */}
<div className="space-y-4">
<Card className="p-4 space-y-4">
<div>
<h3 className="font-semibold text-lg">{asset.name}</h3>
{asset.description && (
<p className="text-sm text-muted-foreground mt-1">
{asset.description}
</p>
)}
</div>
<Separator />
{asset.status && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Status</span>
<Badge
variant="secondary"
className={getStatusColor(asset.status)}
>
{asset.status}
</Badge>
</div>
)}
<div className="space-y-3">
<MetaRow label="File Size" value={formatFileSize(asset.fileSize)} />
<MetaRow label="MIME Type" value={asset.mimeType ?? "—"} />
<MetaRow
label="Revision"
value={asset.currentRevision?.toString() ?? "—"}
/>
<MetaRow label="Created" value={formatDate(asset.creationDate)} />
<MetaRow
label="Modified"
value={formatDate(asset.lastModificationDate)}
/>
</div>
<Separator />
{asset.project && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Project</span>
<Link
href={`/projects/${asset.project.id}`}
className="text-sm font-medium text-primary hover:underline"
>
{asset.project.name}
</Link>
</div>
)}
{asset.folder && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Folder</span>
<span className="text-sm">{asset.folder.name}</span>
</div>
)}
{asset.folder?.path && (
<div>
<span className="text-sm text-muted-foreground">Path</span>
<p className="text-xs font-mono text-muted-foreground mt-1 break-all">
{asset.folder.path}
</p>
</div>
)}
</Card>
{asset.metadatas && asset.metadatas.length > 0 && (
<Card className="p-4">
<h4 className="font-medium text-sm mb-3">Metadata</h4>
<div className="space-y-2">
{asset.metadatas.map((m) => (
<MetaRow key={m.name} label={m.name} value={m.value} />
))}
</div>
</Card>
)}
</div>
</div>
);
}
function MetaRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">{label}</span>
<span className="text-sm font-medium">{value}</span>
</div>
);
}

View file

@ -0,0 +1,87 @@
"use client";
import { useState } from "react";
import type { Asset } from "@/lib/dalim-types";
import { AssetCard } from "./asset-card";
import { Skeleton } from "@/components/ui/skeleton";
import { SendToDialog } from "@/components/distribution/send-to-dialog";
import Link from "next/link";
export function AssetGrid({
assets,
isLoading,
linkPrefix = "/assets",
}: {
assets?: Asset[];
isLoading: boolean;
linkPrefix?: string;
}) {
const [sendToAsset, setSendToAsset] = useState<Asset | null>(null);
if (isLoading) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="aspect-[4/3] w-full rounded-lg" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</div>
))}
</div>
);
}
if (!assets || assets.length === 0) {
return (
<div className="flex items-center justify-center h-48 text-muted-foreground">
No assets found
</div>
);
}
return (
<>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{assets.map((asset) => (
<div key={asset.id} className="relative group">
<Link href={`${linkPrefix}/${asset.id}`}>
<AssetCard asset={asset} />
</Link>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setSendToAsset(asset);
}}
className="absolute top-2 right-2 p-1.5 bg-black/60 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity hover:bg-primary"
title="Send To"
>
<svg
className="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
/>
</svg>
</button>
</div>
))}
</div>
{sendToAsset && (
<SendToDialog
assetId={sendToAsset.id}
assetName={sendToAsset.name}
isOpen={true}
onClose={() => setSendToAsset(null)}
/>
)}
</>
);
}

View file

@ -0,0 +1,173 @@
"use client";
import { useState } from "react";
import { useChannels, useSendToChannel } from "@/hooks/use-dalim";
import type { DistributionChannel } from "@/lib/dalim-types";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
const categoryLabels: Record<string, string> = {
pim: "Product Data",
social: "Social Media",
web: "Web & App",
instore: "In-Store",
print: "Print",
advertising: "Advertising",
google: "Google",
};
const categoryOrder = ["web", "social", "google", "advertising", "pim", "instore", "print"];
const channelIcons: Record<string, React.ReactNode> = {
database: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /></svg>,
globe: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" /></svg>,
smartphone: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" /></svg>,
"shopping-cart": <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" /></svg>,
facebook: <svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>,
video: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>,
linkedin: <svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>,
google: <svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" /><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" /><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" /><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" /></svg>,
youtube: <svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M23.498 6.186a3.016 3.016 0 00-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 00.502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 002.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 002.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>,
tag: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" /><path strokeLinecap="round" strokeLinejoin="round" d="M6 6h.008v.008H6V6z" /></svg>,
monitor: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h13.5A2.25 2.25 0 0121 5.25z" /></svg>,
layout: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" /></svg>,
printer: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m0 0a48.103 48.103 0 018.5 0m-8.5 0V5.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v1.5" /></svg>,
mail: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /></svg>,
target: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118 20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>,
store: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}><path strokeLinecap="round" strokeLinejoin="round" d="M13.5 21v-7.5a.75.75 0 01.75-.75h3a.75.75 0 01.75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349m-16.5 11.65V9.35m0 0a3.001 3.001 0 003.75-.615A2.993 2.993 0 009.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 002.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 003.75.614m-16.5 0a3.004 3.004 0 01-.621-4.72L4.318 3.44A1.5 1.5 0 015.378 3h13.243a1.5 1.5 0 011.06.44l1.19 1.189a3 3 0 01-.621 4.72m-13.5 8.65h3.75a.75.75 0 00.75-.75V13.5a.75.75 0 00-.75-.75H6.75a.75.75 0 00-.75.75v3.15c0 .415.336.75.75.75z" /></svg>,
};
interface SendToDialogProps {
assetId: string;
assetName: string;
isOpen: boolean;
onClose: () => void;
}
export function SendToDialog({
assetId,
assetName,
isOpen,
onClose,
}: SendToDialogProps) {
const { data: channels, isLoading } = useChannels();
const sendMutation = useSendToChannel();
const [sentChannels, setSentChannels] = useState<Set<string>>(new Set());
if (!isOpen) return null;
const grouped = channels?.reduce(
(acc, ch) => {
if (!acc[ch.category]) acc[ch.category] = [];
acc[ch.category].push(ch);
return acc;
},
{} as Record<string, DistributionChannel[]>
);
const handleSend = async (channelId: string) => {
await sendMutation.mutateAsync({ assetId, channelId });
setSentChannels((prev) => new Set(prev).add(channelId));
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-background rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-5 border-b">
<div>
<h3 className="text-lg font-semibold">Send To Platform</h3>
<p className="text-sm text-muted-foreground mt-0.5">
Distribute <span className="font-medium">{assetName}</span>
</p>
</div>
<button
onClick={onClose}
className="text-muted-foreground hover:text-foreground p-1"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Content */}
<div className="overflow-y-auto p-5 space-y-6">
{isLoading ? (
<p className="text-sm text-muted-foreground">
Loading channels...
</p>
) : (
categoryOrder.map((cat) => {
const items = grouped?.[cat];
if (!items?.length) return null;
return (
<div key={cat}>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
{categoryLabels[cat] ?? cat}
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{items.map((ch) => {
const isSent = sentChannels.has(ch.id);
const isDisabled =
ch.status === "coming_soon" || isSent;
return (
<button
key={ch.id}
onClick={() => handleSend(ch.id)}
disabled={isDisabled || sendMutation.isPending}
className={cn(
"flex items-center gap-3 p-3 rounded-lg border text-left transition-all",
isDisabled
? "opacity-50 cursor-not-allowed bg-muted"
: "hover:border-primary hover:bg-primary/5 cursor-pointer"
)}
>
<div className="shrink-0 text-muted-foreground">
{channelIcons[ch.icon] ?? channelIcons.globe}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
{ch.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{ch.description}
</p>
</div>
{isSent ? (
<Badge className="bg-green-100 text-green-800 shrink-0">
Sent
</Badge>
) : ch.status === "coming_soon" ? (
<Badge variant="outline" className="shrink-0 text-xs">
Soon
</Badge>
) : ch.status === "available" ? (
<Badge variant="outline" className="shrink-0 text-xs">
Setup
</Badge>
) : null}
</button>
);
})}
</div>
</div>
);
})
)}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,60 @@
"use client";
import { Component, type ReactNode } from "react";
import { Card } from "@/components/ui/card";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<Card className="p-6 text-center">
<svg
className="w-12 h-12 mx-auto text-destructive/50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
/>
</svg>
<h3 className="text-lg font-semibold mt-3">Something went wrong</h3>
<p className="text-sm text-muted-foreground mt-1">
{this.state.error?.message ?? "An unexpected error occurred"}
</p>
<button
onClick={() => this.setState({ hasError: false })}
className="mt-4 px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm"
>
Try again
</button>
</Card>
);
}
return this.props.children;
}
}

View file

@ -0,0 +1,91 @@
"use client";
import type { Folder } from "@/lib/dalim-types";
import { cn } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
interface FolderTreeProps {
folders?: Folder[];
isLoading?: boolean;
selectedId?: string;
onSelect: (folderId: string | undefined) => void;
}
export function FolderTree({
folders,
isLoading,
selectedId,
onSelect,
}: FolderTreeProps) {
if (isLoading) {
return (
<div className="space-y-2 p-2">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-full" />
))}
</div>
);
}
if (!folders || folders.length === 0) {
return (
<p className="text-sm text-muted-foreground p-3">No folders</p>
);
}
return (
<div className="space-y-0.5 p-1">
<button
onClick={() => onSelect(undefined)}
className={cn(
"flex items-center gap-2 w-full px-3 py-2 rounded-md text-sm transition-colors text-left",
!selectedId
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted"
)}
>
<svg
className="w-4 h-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
/>
</svg>
All Files
</button>
{folders.map((folder) => (
<button
key={folder.id}
onClick={() => onSelect(folder.id)}
className={cn(
"flex items-center gap-2 w-full px-3 py-2 rounded-md text-sm transition-colors text-left",
selectedId === folder.id
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:bg-muted"
)}
>
<svg
className="w-4 h-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
/>
</svg>
<span className="truncate">{folder.name}</span>
</button>
))}
</div>
);
}

View file

@ -0,0 +1,34 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
export function Header() {
const [searchQuery, setSearchQuery] = useState("");
const router = useRouter();
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
}
};
return (
<header className="h-16 border-b bg-card px-6 flex items-center justify-between sticky top-0 z-10">
<form onSubmit={handleSearch} className="w-full max-w-md">
<Input
type="search"
placeholder="Search assets..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full"
/>
</form>
<div className="flex items-center gap-4 ml-4">
<span className="text-sm text-muted-foreground">ES FUSiON</span>
</div>
</header>
);
}

View file

@ -0,0 +1,113 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
const navItems = [
{ href: "/", label: "Dashboard", icon: "LayoutDashboard" },
{ href: "/projects", label: "Projects", icon: "FolderKanban" },
{ href: "/assets", label: "Assets", icon: "FileImage" },
{ href: "/search", label: "Search", icon: "Search" },
{ href: "/collections", label: "Collections", icon: "Library" },
{ href: "/workflows", label: "Workflows", icon: "GitBranch" },
{ href: "/processes", label: "Processes", icon: "Activity" },
];
const icons: Record<string, React.ReactNode> = {
LayoutDashboard: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
),
FolderKanban: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
</svg>
),
FileImage: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
),
Search: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
),
Library: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6.878V6a2.25 2.25 0 012.25-2.25h7.5A2.25 2.25 0 0118 6v.878m-12 0c.235-.083.487-.128.75-.128h10.5c.263 0 .515.045.75.128m-12 0A2.25 2.25 0 004.5 9v.878m13.5-3A2.25 2.25 0 0119.5 9v.878m0 0a2.246 2.246 0 00-.75-.128H5.25c-.263 0-.515.045-.75.128m15 0A2.25 2.25 0 0121 12v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6c0-.98.626-1.813 1.5-2.122" />
</svg>
),
GitBranch: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 3v12m0 0a3 3 0 103 3H15a3 3 0 100-6H9a3 3 0 01-3-3zm12 0a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
Activity: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg>
),
};
export function Sidebar() {
const pathname = usePathname();
return (
<aside className="w-64 bg-[#1a1a1a] h-screen sticky top-0 flex flex-col">
{/* Logo area */}
<div className="p-5 border-b border-[#333]">
<Link href="/" className="block">
<Image
src="/logo.svg"
alt="Logo"
width={160}
height={22}
className="brightness-100"
priority
/>
</Link>
<p className="text-[10px] text-gray-500 mt-2 uppercase tracking-widest">
Digital Asset Management
</p>
</div>
{/* Navigation */}
<nav className="flex-1 p-3 space-y-0.5 mt-2">
{navItems.map((item) => {
const isActive =
item.href === "/"
? pathname === "/"
: pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors",
isActive
? "bg-[#df0000] text-white"
: "text-gray-400 hover:bg-[#2a2a2a] hover:text-white"
)}
>
{icons[item.icon]}
{item.label}
</Link>
);
})}
</nav>
{/* Footer */}
<div className="p-4 border-t border-[#333]">
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="w-2 h-2 rounded-full bg-yellow-500" />
Mock Mode
</div>
<p className="text-[10px] text-gray-600 mt-1">ES FUSiON API</p>
</div>
</aside>
);
}

View file

@ -0,0 +1,22 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
refetchOnWindowFocus: false,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

View file

@ -0,0 +1,109 @@
"use client"
import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
size = "default",
...props
}: AvatarPrimitive.Root.Props & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className
)}
{...props}
/>
)
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-full object-cover",
className
)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: AvatarPrimitive.Fallback.Props) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}

View file

@ -0,0 +1,52 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
return useRender({
defaultTagName: "span",
props: mergeProps<"span">(
{
className: cn(badgeVariants({ variant }), className),
},
props
),
render,
state: {
slot: "badge",
variant,
},
})
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,60 @@
"use client"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View file

@ -0,0 +1,268 @@
"use client"
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "@/lib/utils"
import { ChevronRightIcon, CheckIcon } from "lucide-react"
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
}
function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
className={cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
)
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}
function DropdownMenuLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
className
)}
{...props}
/>
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
)
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn("w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon
/>
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean
}) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<CheckIcon
/>
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
)
}
function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View file

@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

View file

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: ScrollAreaPrimitive.Root.Props) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: ScrollAreaPrimitive.Scrollbar.Props) {
return (
<ScrollAreaPrimitive.Scrollbar
data-slot="scroll-area-scrollbar"
data-orientation={orientation}
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.Thumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.Scrollbar>
)
}
export { ScrollArea, ScrollBar }

View file

@ -0,0 +1,25 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }

View file

@ -0,0 +1,138 @@
"use client"
import * as React from "react"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
data-slot="sheet-close"
render={
<Button
variant="ghost"
className="absolute top-3 right-3"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-0.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"font-heading text-base font-medium text-foreground",
className
)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: SheetPrimitive.Description.Props) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View file

@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View file

@ -0,0 +1,66 @@
"use client"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delay = 0,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
{...props}
/>
)
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View file

@ -0,0 +1,189 @@
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type {
Project,
Folder,
Asset,
Collection,
Workflow,
Approval,
ProcessMonitorEntry,
DistributionChannel,
DistributionJob,
PagingResponse,
SearchResponse,
} from "@/lib/dalim-types";
async function fetchAPI<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export function useProjects(limit = 20) {
return useQuery({
queryKey: ["projects", limit],
queryFn: () =>
fetchAPI<PagingResponse<Project>>(`/api/projects?limit=${limit}`),
});
}
export function useProject(id: string) {
return useQuery({
queryKey: ["project", id],
queryFn: () => fetchAPI<Project>(`/api/projects/${id}`),
enabled: !!id,
});
}
export function useAssets(limit = 24) {
return useQuery({
queryKey: ["assets", limit],
queryFn: () =>
fetchAPI<PagingResponse<Asset>>(`/api/assets?limit=${limit}`),
});
}
export function useAsset(id: string) {
return useQuery({
queryKey: ["asset", id],
queryFn: () => fetchAPI<Asset>(`/api/assets/${id}`),
enabled: !!id,
});
}
export function useSearch(query: string, limit = 24) {
return useQuery({
queryKey: ["search", query, limit],
queryFn: () =>
fetchAPI<SearchResponse>(
`/api/search?q=${encodeURIComponent(query)}&limit=${limit}`
),
enabled: query.length > 0,
});
}
export function useWorkflows(limit = 20) {
return useQuery({
queryKey: ["workflows", limit],
queryFn: () =>
fetchAPI<PagingResponse<Workflow>>(`/api/workflows?limit=${limit}`),
});
}
export function useApprovals(limit = 20) {
return useQuery({
queryKey: ["approvals", limit],
queryFn: () =>
fetchAPI<PagingResponse<Approval>>(`/api/approvals?limit=${limit}`),
});
}
export function useCollections(limit = 20) {
return useQuery({
queryKey: ["collections", limit],
queryFn: () =>
fetchAPI<PagingResponse<Collection>>(`/api/collections?limit=${limit}`),
});
}
export function useProcesses(limit = 20) {
return useQuery({
queryKey: ["processes", limit],
queryFn: () =>
fetchAPI<PagingResponse<ProcessMonitorEntry>>(
`/api/processes?limit=${limit}`
),
});
}
// === Phase 2: Project detail & folder browsing ===
export function useProjectFolders(projectId: string) {
return useQuery({
queryKey: ["project-folders", projectId],
queryFn: () => fetchAPI<Folder[]>(`/api/projects/${projectId}/folders`),
enabled: !!projectId,
});
}
export function useProjectAssets(
projectId: string,
folderId?: string,
limit = 24
) {
return useQuery({
queryKey: ["project-assets", projectId, folderId, limit],
queryFn: () => {
const params = new URLSearchParams({ limit: String(limit) });
if (folderId) params.set("folderId", folderId);
return fetchAPI<PagingResponse<Asset>>(
`/api/projects/${projectId}/assets?${params}`
);
},
enabled: !!projectId,
});
}
export function useFolderAssets(folderId: string, limit = 24) {
return useQuery({
queryKey: ["folder-assets", folderId, limit],
queryFn: () =>
fetchAPI<PagingResponse<Asset>>(
`/api/folders/${folderId}/assets?limit=${limit}`
),
enabled: !!folderId,
});
}
export function useFolder(id: string) {
return useQuery({
queryKey: ["folder", id],
queryFn: () => fetchAPI<Folder>(`/api/folders/${id}`),
enabled: !!id,
});
}
// === Distribution / Send-to ===
export function useChannels() {
return useQuery({
queryKey: ["channels"],
queryFn: () => fetchAPI<DistributionChannel[]>("/api/channels"),
});
}
export function useDistributionJobs(assetId?: string) {
return useQuery({
queryKey: ["distribution-jobs", assetId],
queryFn: () => {
const params = assetId ? `?assetId=${assetId}` : "";
return fetchAPI<DistributionJob[]>(`/api/distribution${params}`);
},
});
}
export function useSendToChannel() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
assetId,
channelId,
}: {
assetId: string;
channelId: string;
}) => {
const res = await fetch("/api/distribution", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ assetId, channelId }),
});
if (!res.ok) throw new Error("Failed to send");
return res.json() as Promise<DistributionJob>;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["distribution-jobs"] });
},
});
}

View file

@ -0,0 +1,68 @@
import { GraphQLClient } from "graphql-request";
interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
let cachedToken: string | null = null;
let tokenExpiry: number = 0;
function isMockMode(): boolean {
return process.env.DALIM_MOCK_MODE === "true";
}
async function getAccessToken(): Promise<string> {
if (cachedToken && Date.now() < tokenExpiry) {
return cachedToken;
}
const tokenUrl = process.env.DALIM_TOKEN_URL!;
const response = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "password",
client_id: process.env.DALIM_CLIENT_ID!,
client_secret: process.env.DALIM_CLIENT_SECRET!,
username: process.env.DALIM_USERNAME!,
password: process.env.DALIM_PASSWORD!,
}),
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status} ${response.statusText}`);
}
const data: TokenResponse = await response.json();
cachedToken = data.access_token;
// Expire 60 seconds early to avoid edge cases
tokenExpiry = Date.now() + (data.expires_in - 60) * 1000;
return cachedToken;
}
export function getDalimClient(): GraphQLClient {
const url = process.env.DALIM_GRAPHQL_URL!;
return new GraphQLClient(url);
}
export async function dalimQuery<T>(
query: string,
variables?: Record<string, unknown>
): Promise<T> {
if (isMockMode()) {
throw new Error("MOCK_MODE: Use dalimMockQuery instead");
}
const token = await getAccessToken();
const client = getDalimClient();
return client.request<T>(query, variables, {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
});
}
export { isMockMode };

View file

@ -0,0 +1,223 @@
// GraphQL query strings for Dalim ES FUSiON API
// Reference: ../docs/dalim-api-reference.md
export const QUERIES = {
// === System ===
whoami: `query { whoami { login firstName lastName email } }`,
serverInformation: `query { serverInformation { version build } }`,
// === Projects ===
projects: `
query projects($filter: iFilter, $limit: Int, $cursor: ID) {
projects(filter: $filter, limit: $limit, cursor: $cursor) {
items {
id
name
description
status
creationDate
lastModificationDate
customer { id name }
}
cursor
totalCount
}
}
`,
projectById: `
query projectById($id: [ID!]!) {
projectById(id: $id) {
id
name
description
status
creationDate
lastModificationDate
customer { id name }
metadatas { name value }
}
}
`,
// === Folders ===
folders: `
query folders($filter: iFilter, $limit: Int, $cursor: ID) {
folders(filter: $filter, limit: $limit, cursor: $cursor) {
items {
id
name
path
creationDate
volume { id name }
}
cursor
totalCount
}
}
`,
folderById: `
query folderById($id: [ID!]!) {
folderById(id: $id) {
id
name
path
creationDate
volume { id name }
}
}
`,
// === Assets ===
assets: `
query assets($filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy) {
assets(filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy) {
items {
id
name
description
creationDate
lastModificationDate
currentRevision
mimeType
fileSize
status
folder { id name }
project { id name }
}
cursor
totalCount
}
}
`,
assetById: `
query assetById($id: [ID!]!) {
assetById(id: $id) {
id
name
description
creationDate
lastModificationDate
currentRevision
mimeType
fileSize
status
folder { id name path }
project { id name }
metadatas { name value }
}
}
`,
// === Search ===
search: `
query search($filter: iSearchFilter!, $facets: [String!]!, $limit: Int, $cursor: ID) {
search(filter: $filter, facets: $facets, limit: $limit, cursor: $cursor) {
items {
id
name
description
type
mimeType
creationDate
lastModificationDate
}
totalCount
cursor
facets { name values { value count } }
}
}
`,
// === Workflows ===
workflows: `
query workflows($filter: iFilter, $limit: Int, $cursor: ID) {
workflows(filter: $filter, limit: $limit, cursor: $cursor) {
items {
id
name
description
revision
}
cursor
totalCount
}
}
`,
workflowsByEntity: `
query workflowsByEntity($entityId: ID!) {
workflowsByEntity(entityId: $entityId) {
id
name
revision
}
}
`,
// === Approvals ===
approvals: `
query approvals($filter: iFilter, $limit: Int, $cursor: ID) {
approvals(filter: $filter, limit: $limit, cursor: $cursor) {
items {
id
status
entity { id name type }
approver { id login firstName lastName }
level
}
cursor
totalCount
}
}
`,
// === Collections ===
collections: `
query collections($filter: iFilter, $limit: Int, $cursor: ID) {
collections(filter: $filter, limit: $limit, cursor: $cursor) {
items {
id
name
description
creationDate
}
cursor
totalCount
}
}
`,
collectionById: `
query collectionById($id: [ID!]!) {
collectionById(id: $id) {
id
name
description
creationDate
}
}
`,
// === Process Monitoring ===
processMonitoring: `
query processMonitoring($filter: iFilter, $limit: Int, $cursor: ID) {
processMonitoring(filter: $filter, limit: $limit, cursor: $cursor) {
items {
id
name
status
progress
startDate
endDate
entity { id name type }
}
cursor
totalCount
}
}
`,
} as const;

View file

@ -0,0 +1,351 @@
// Unified service layer — returns mock or real data based on DALIM_MOCK_MODE
import { isMockMode, dalimQuery } from "./dalim-client";
import { QUERIES } from "./dalim-queries";
import {
mockProjects,
mockFolders,
mockAssets,
mockCollections,
mockWorkflows,
mockApprovals,
mockProcesses,
mockChannels,
mockDistributionJobs,
folderProjectMap,
} from "./mock/data";
import type {
Project,
Folder,
Asset,
Collection,
Workflow,
Approval,
ProcessMonitorEntry,
DistributionChannel,
DistributionJob,
PagingResponse,
SearchResponse,
} from "./dalim-types";
// === Projects ===
export async function getProjects(
limit = 20,
cursor?: string
): Promise<PagingResponse<Project>> {
if (isMockMode()) {
return {
items: mockProjects,
cursor: null,
totalCount: mockProjects.length,
};
}
const data = await dalimQuery<{ projects: PagingResponse<Project> }>(
QUERIES.projects,
{ limit, cursor }
);
return data.projects;
}
export async function getProjectById(id: string): Promise<Project | null> {
if (isMockMode()) {
return mockProjects.find((p) => p.id === id) ?? null;
}
const data = await dalimQuery<{ projectById: Project[] }>(
QUERIES.projectById,
{ id: [id] }
);
return data.projectById?.[0] ?? null;
}
// === Folders ===
export async function getFolders(
limit = 20,
cursor?: string
): Promise<PagingResponse<Folder>> {
if (isMockMode()) {
return {
items: mockFolders,
cursor: null,
totalCount: mockFolders.length,
};
}
const data = await dalimQuery<{ folders: PagingResponse<Folder> }>(
QUERIES.folders,
{ limit, cursor }
);
return data.folders;
}
// === Assets ===
export async function getAssets(
limit = 24,
cursor?: string,
filter?: Record<string, unknown>
): Promise<PagingResponse<Asset>> {
if (isMockMode()) {
return {
items: mockAssets,
cursor: null,
totalCount: mockAssets.length,
};
}
const data = await dalimQuery<{ assets: PagingResponse<Asset> }>(
QUERIES.assets,
{ limit, cursor, filter }
);
return data.assets;
}
export async function getAssetById(id: string): Promise<Asset | null> {
if (isMockMode()) {
return mockAssets.find((a) => a.id === id) ?? null;
}
const data = await dalimQuery<{ assetById: Asset[] }>(
QUERIES.assetById,
{ id: [id] }
);
return data.assetById?.[0] ?? null;
}
// === Search ===
export async function searchAssets(
query: string,
facets: string[] = [],
limit = 24,
cursor?: string
): Promise<SearchResponse> {
if (isMockMode()) {
const filtered = mockAssets.filter(
(a) =>
a.name.toLowerCase().includes(query.toLowerCase()) ||
a.description?.toLowerCase().includes(query.toLowerCase())
);
return {
items: filtered.map((a) => ({
id: a.id,
name: a.name,
description: a.description,
type: "Asset",
mimeType: a.mimeType,
creationDate: a.creationDate,
lastModificationDate: a.lastModificationDate,
})),
totalCount: filtered.length,
cursor: null,
facets: [
{
name: "mimeType",
values: [
{ value: "application/pdf", count: 3 },
{ value: "image/tiff", count: 1 },
{ value: "application/illustrator", count: 1 },
],
},
{
name: "status",
values: [
{ value: "APPROVED", count: 3 },
{ value: "IN_REVIEW", count: 2 },
{ value: "PENDING", count: 2 },
],
},
],
};
}
const data = await dalimQuery<{ search: SearchResponse }>(QUERIES.search, {
filter: { query },
facets,
limit,
cursor,
});
return data.search;
}
// === Workflows ===
export async function getWorkflows(
limit = 20,
cursor?: string
): Promise<PagingResponse<Workflow>> {
if (isMockMode()) {
return {
items: mockWorkflows,
cursor: null,
totalCount: mockWorkflows.length,
};
}
const data = await dalimQuery<{ workflows: PagingResponse<Workflow> }>(
QUERIES.workflows,
{ limit, cursor }
);
return data.workflows;
}
// === Approvals ===
export async function getApprovals(
limit = 20,
cursor?: string
): Promise<PagingResponse<Approval>> {
if (isMockMode()) {
return {
items: mockApprovals,
cursor: null,
totalCount: mockApprovals.length,
};
}
const data = await dalimQuery<{ approvals: PagingResponse<Approval> }>(
QUERIES.approvals,
{ limit, cursor }
);
return data.approvals;
}
// === Processes ===
export async function getProcesses(
limit = 20,
cursor?: string
): Promise<PagingResponse<ProcessMonitorEntry>> {
if (isMockMode()) {
return {
items: mockProcesses,
cursor: null,
totalCount: mockProcesses.length,
};
}
const data = await dalimQuery<{
processMonitoring: PagingResponse<ProcessMonitorEntry>;
}>(QUERIES.processMonitoring, { limit, cursor });
return data.processMonitoring;
}
// === Collections ===
export async function getCollections(
limit = 20,
cursor?: string
): Promise<PagingResponse<Collection>> {
if (isMockMode()) {
return {
items: mockCollections,
cursor: null,
totalCount: mockCollections.length,
};
}
const data = await dalimQuery<{ collections: PagingResponse<Collection> }>(
QUERIES.collections,
{ limit, cursor }
);
return data.collections;
}
// === Project-specific queries ===
export async function getFoldersByProject(
projectId: string
): Promise<Folder[]> {
if (isMockMode()) {
const folderIds = Object.entries(folderProjectMap)
.filter(([, pId]) => pId === projectId)
.map(([fId]) => fId);
return mockFolders.filter((f) => folderIds.includes(f.id));
}
// Real API: use folders query with project filter
const data = await dalimQuery<{ folders: PagingResponse<Folder> }>(
QUERIES.folders,
{ filter: { projectId }, limit: 100 }
);
return data.folders.items;
}
export async function getAssetsByProject(
projectId: string,
folderId?: string,
limit = 24,
cursor?: string
): Promise<PagingResponse<Asset>> {
if (isMockMode()) {
let filtered = mockAssets.filter((a) => a.project?.id === projectId);
if (folderId) {
filtered = filtered.filter((a) => a.folder?.id === folderId);
}
return { items: filtered, cursor: null, totalCount: filtered.length };
}
const filter: Record<string, unknown> = { projectId };
if (folderId) filter.folderId = folderId;
const data = await dalimQuery<{ assets: PagingResponse<Asset> }>(
QUERIES.assets,
{ filter, limit, cursor }
);
return data.assets;
}
export async function getAssetsByFolder(
folderId: string,
limit = 24,
cursor?: string
): Promise<PagingResponse<Asset>> {
if (isMockMode()) {
const filtered = mockAssets.filter((a) => a.folder?.id === folderId);
return { items: filtered, cursor: null, totalCount: filtered.length };
}
const data = await dalimQuery<{ assets: PagingResponse<Asset> }>(
QUERIES.assets,
{ filter: { folderId }, limit, cursor }
);
return data.assets;
}
export async function getFolderById(id: string): Promise<Folder | null> {
if (isMockMode()) {
return mockFolders.find((f) => f.id === id) ?? null;
}
const data = await dalimQuery<{ folderById: Folder[] }>(
QUERIES.folderById,
{ id: [id] }
);
return data.folderById?.[0] ?? null;
}
// === Distribution Channels ===
export async function getChannels(): Promise<DistributionChannel[]> {
// Mock-only for now — real API would use a custom integration layer
return mockChannels;
}
export async function getDistributionJobs(
assetId?: string
): Promise<DistributionJob[]> {
let jobs = mockDistributionJobs;
if (assetId) {
jobs = jobs.filter((j) => j.assetId === assetId);
}
return jobs;
}
export async function sendToChannel(
assetId: string,
channelId: string
): Promise<DistributionJob> {
const asset = mockAssets.find((a) => a.id === assetId);
const channel = mockChannels.find((c) => c.id === channelId);
const job: DistributionJob = {
id: `DJ${Date.now()}`,
assetId,
assetName: asset?.name ?? assetId,
channelId,
channelName: channel?.name ?? channelId,
status: "queued",
initiatedBy: "dporter",
initiatedAt: new Date().toISOString(),
};
mockDistributionJobs.push(job);
return job;
}

View file

@ -0,0 +1,148 @@
// TypeScript types for Dalim ES FUSiON API responses
export interface PagingResponse<T> {
items: T[];
cursor: string | null;
totalCount: number;
}
export interface FacetValue {
value: string;
count: number;
}
export interface Facet {
name: string;
values: FacetValue[];
}
// === Core Entities ===
export interface Project {
id: string;
name: string;
description?: string;
status?: string;
creationDate?: string;
lastModificationDate?: string;
customer?: { id: string; name: string };
metadatas?: MetadataValue[];
}
export interface Folder {
id: string;
name: string;
path?: string;
creationDate?: string;
volume?: { id: string; name: string };
}
export interface Asset {
id: string;
name: string;
description?: string;
creationDate?: string;
lastModificationDate?: string;
currentRevision?: number;
mimeType?: string;
fileSize?: number;
status?: string;
folder?: { id: string; name: string; path?: string };
project?: { id: string; name: string };
metadatas?: MetadataValue[];
}
export interface MetadataValue {
name: string;
value: string;
}
export interface Collection {
id: string;
name: string;
description?: string;
creationDate?: string;
}
// === Workflow & Approval ===
export interface Workflow {
id: string;
name: string;
description?: string;
revision?: number;
}
export interface Approval {
id: string;
status: string;
entity: { id: string; name: string; type: string };
approver: { id: string; login: string; firstName: string; lastName: string };
level: number;
}
export interface ProcessMonitorEntry {
id: string;
name: string;
status: string;
progress?: number;
startDate?: string;
endDate?: string;
entity?: { id: string; name: string; type: string };
}
// === Search ===
export interface SearchResult {
id: string;
name: string;
description?: string;
type: string;
mimeType?: string;
creationDate?: string;
lastModificationDate?: string;
}
export interface SearchResponse {
items: SearchResult[];
totalCount: number;
cursor: string | null;
facets: Facet[];
}
// === Distribution / Send-to Destinations ===
export interface DistributionChannel {
id: string;
name: string;
category: "pim" | "social" | "web" | "instore" | "print" | "advertising" | "google";
icon: string;
description: string;
status: "connected" | "available" | "coming_soon";
}
export interface DistributionJob {
id: string;
assetId: string;
assetName: string;
channelId: string;
channelName: string;
status: "queued" | "processing" | "completed" | "failed";
initiatedBy: string;
initiatedAt: string;
completedAt?: string;
}
// === Auth ===
export interface WhoAmI {
login: string;
firstName: string;
lastName: string;
email: string;
}
export interface ServerInfo {
version: string;
build: string;
}

View file

@ -0,0 +1,305 @@
import type {
Project,
Folder,
Asset,
Collection,
Workflow,
Approval,
ProcessMonitorEntry,
SearchResult,
DistributionChannel,
DistributionJob,
} from "@/lib/dalim-types";
// === Mock Projects ===
export const mockProjects: Project[] = [
{
id: "P001",
name: "Spring Campaign 2024",
description: "Marketing assets for spring product launch",
status: "ACTIVE",
creationDate: "2024-01-15T10:00:00Z",
lastModificationDate: "2024-03-20T14:30:00Z",
customer: { id: "C001", name: "Acme Corp" },
},
{
id: "P002",
name: "Product Packaging v3",
description: "Updated packaging design files for Q2 release",
status: "ACTIVE",
creationDate: "2024-02-01T09:00:00Z",
lastModificationDate: "2024-03-18T11:00:00Z",
customer: { id: "C001", name: "Acme Corp" },
},
{
id: "P003",
name: "Annual Report 2023",
description: "Corporate annual report design and print files",
status: "COMPLETED",
creationDate: "2023-11-01T08:00:00Z",
lastModificationDate: "2024-01-10T16:00:00Z",
customer: { id: "C002", name: "Global Industries" },
},
{
id: "P004",
name: "Website Rebrand",
description: "New brand identity assets for website refresh",
status: "ACTIVE",
creationDate: "2024-03-01T10:00:00Z",
lastModificationDate: "2024-03-25T09:00:00Z",
customer: { id: "C003", name: "TechStart Inc" },
},
{
id: "P005",
name: "Trade Show Materials",
description: "Booth graphics, brochures, and digital displays",
status: "IN_REVIEW",
creationDate: "2024-02-15T14:00:00Z",
lastModificationDate: "2024-03-22T10:00:00Z",
customer: { id: "C002", name: "Global Industries" },
},
];
// === Folder → Project mapping ===
export const folderProjectMap: Record<string, string> = {
F001: "P001", F002: "P001", F003: "P001",
F004: "P002", F005: "P002",
F006: "P003", F007: "P004", F008: "P004",
F009: "P005", F010: "P005",
};
// === Mock Folders ===
export const mockFolders: Folder[] = [
{ id: "F001", name: "Print Ready", path: "/Spring Campaign 2024/Print Ready", creationDate: "2024-01-15T10:00:00Z" },
{ id: "F002", name: "Digital", path: "/Spring Campaign 2024/Digital", creationDate: "2024-01-15T10:00:00Z" },
{ id: "F003", name: "Photography", path: "/Spring Campaign 2024/Photography", creationDate: "2024-01-20T09:00:00Z" },
{ id: "F004", name: "Artwork", path: "/Product Packaging v3/Artwork", creationDate: "2024-02-01T09:00:00Z" },
{ id: "F005", name: "Proofs", path: "/Product Packaging v3/Proofs", creationDate: "2024-02-05T11:00:00Z" },
{ id: "F006", name: "Final Files", path: "/Annual Report 2023/Final Files", creationDate: "2023-12-01T09:00:00Z" },
{ id: "F007", name: "Logos", path: "/Website Rebrand/Logos", creationDate: "2024-03-01T10:00:00Z" },
{ id: "F008", name: "Style Guide", path: "/Website Rebrand/Style Guide", creationDate: "2024-03-02T10:00:00Z" },
{ id: "F009", name: "Booth Graphics", path: "/Trade Show Materials/Booth Graphics", creationDate: "2024-02-15T14:00:00Z" },
{ id: "F010", name: "Brochures", path: "/Trade Show Materials/Brochures", creationDate: "2024-02-15T14:00:00Z" },
];
// === Mock Assets ===
export const mockAssets: Asset[] = [
{
id: "A001",
name: "hero-banner-spring-v3.pdf",
description: "Main hero banner for spring campaign landing page",
creationDate: "2024-01-20T10:00:00Z",
lastModificationDate: "2024-03-15T14:30:00Z",
currentRevision: 3,
mimeType: "application/pdf",
fileSize: 4500000,
status: "APPROVED",
folder: { id: "F001", name: "Print Ready", path: "/Spring Campaign 2024/Print Ready" },
project: { id: "P001", name: "Spring Campaign 2024" },
},
{
id: "A002",
name: "product-shot-01.tif",
description: "Main product photography - front angle",
creationDate: "2024-02-10T09:00:00Z",
lastModificationDate: "2024-03-10T11:00:00Z",
currentRevision: 1,
mimeType: "image/tiff",
fileSize: 85000000,
status: "APPROVED",
folder: { id: "F003", name: "Photography" },
project: { id: "P001", name: "Spring Campaign 2024" },
},
{
id: "A003",
name: "packaging-front-v2.ai",
description: "Front panel design for primary packaging",
creationDate: "2024-02-05T14:00:00Z",
lastModificationDate: "2024-03-20T16:00:00Z",
currentRevision: 2,
mimeType: "application/illustrator",
fileSize: 12000000,
status: "IN_REVIEW",
folder: { id: "F004", name: "Artwork" },
project: { id: "P002", name: "Product Packaging v3" },
},
{
id: "A004",
name: "social-media-kit.zip",
description: "Complete social media asset package - all sizes",
creationDate: "2024-03-01T10:00:00Z",
lastModificationDate: "2024-03-18T15:00:00Z",
currentRevision: 1,
mimeType: "application/zip",
fileSize: 25000000,
status: "PENDING",
folder: { id: "F002", name: "Digital" },
project: { id: "P001", name: "Spring Campaign 2024" },
},
{
id: "A005",
name: "annual-report-final.indd",
description: "Final InDesign file for annual report",
creationDate: "2023-12-15T09:00:00Z",
lastModificationDate: "2024-01-08T14:00:00Z",
currentRevision: 5,
mimeType: "application/x-indesign",
fileSize: 150000000,
status: "APPROVED",
folder: { id: "F001", name: "Print Ready" },
project: { id: "P003", name: "Annual Report 2023" },
},
{
id: "A006",
name: "booth-backdrop-3m.pdf",
description: "3-meter booth backdrop for trade show",
creationDate: "2024-02-20T11:00:00Z",
lastModificationDate: "2024-03-22T09:00:00Z",
currentRevision: 2,
mimeType: "application/pdf",
fileSize: 35000000,
status: "IN_REVIEW",
folder: { id: "F005", name: "Proofs" },
project: { id: "P005", name: "Trade Show Materials" },
},
{
id: "A007",
name: "logo-rebrand-primary.svg",
description: "New primary logo - full color",
creationDate: "2024-03-05T10:00:00Z",
lastModificationDate: "2024-03-25T08:00:00Z",
currentRevision: 4,
mimeType: "image/svg+xml",
fileSize: 45000,
status: "APPROVED",
folder: { id: "F002", name: "Digital" },
project: { id: "P004", name: "Website Rebrand" },
},
{
id: "A008",
name: "brochure-trifold-v1.pdf",
description: "Tri-fold brochure for trade show distribution",
creationDate: "2024-03-10T14:00:00Z",
lastModificationDate: "2024-03-21T16:00:00Z",
currentRevision: 1,
mimeType: "application/pdf",
fileSize: 8500000,
status: "PENDING",
folder: { id: "F005", name: "Proofs" },
project: { id: "P005", name: "Trade Show Materials" },
},
];
// === Mock Collections ===
export const mockCollections: Collection[] = [
{ id: "COL001", name: "Approved for Print", description: "All assets approved for print production", creationDate: "2024-01-01T00:00:00Z" },
{ id: "COL002", name: "Brand Guidelines", description: "Core brand identity assets and guidelines", creationDate: "2024-01-01T00:00:00Z" },
{ id: "COL003", name: "Q2 Deliverables", description: "All deliverables due in Q2 2024", creationDate: "2024-03-01T00:00:00Z" },
];
// === Mock Workflows ===
export const mockWorkflows: Workflow[] = [
{ id: "W001", name: "Print Approval", description: "Standard print approval workflow — preflight, color check, sign-off", revision: 1 },
{ id: "W002", name: "Digital Review", description: "Digital asset review and approval for web & app", revision: 2 },
{ id: "W003", name: "Packaging QC", description: "Quality control for packaging files — dielines, barcode verification", revision: 1 },
{ id: "W004", name: "Send to Social Platforms", description: "Distribute approved assets to Meta, TikTok, LinkedIn", revision: 3 },
{ id: "W005", name: "Send to PIM", description: "Sync product images and data sheets to Stibo STEP", revision: 2 },
{ id: "W006", name: "Send to Google Platforms", description: "Push creatives to Google Ads, Performance Max, CM360, YouTube", revision: 1 },
{ id: "W007", name: "In-Store Distribution", description: "Distribute to ESL, in-store TV, POP displays, and digital screens", revision: 1 },
{ id: "W008", name: "Send to E-Commerce", description: "Publish product assets to website, mobile app, and PWA storefront", revision: 4 },
{ id: "W009", name: "Print Fulfillment", description: "Send to print production — flyers, catalogues, large-format", revision: 2 },
{ id: "W010", name: "Programmatic Distribution", description: "Push ad creatives to DV360 and Trade Desk campaigns", revision: 1 },
];
// === Mock Approvals ===
export const mockApprovals: Approval[] = [
{
id: "AP001",
status: "PENDING",
entity: { id: "A003", name: "packaging-front-v2.ai", type: "Asset" },
approver: { id: "U001", login: "jsmith", firstName: "John", lastName: "Smith" },
level: 1,
},
{
id: "AP002",
status: "PENDING",
entity: { id: "A006", name: "booth-backdrop-3m.pdf", type: "Asset" },
approver: { id: "U002", login: "mwilson", firstName: "Maria", lastName: "Wilson" },
level: 1,
},
{
id: "AP003",
status: "APPROVED",
entity: { id: "A001", name: "hero-banner-spring-v3.pdf", type: "Asset" },
approver: { id: "U001", login: "jsmith", firstName: "John", lastName: "Smith" },
level: 2,
},
{
id: "AP004",
status: "PENDING",
entity: { id: "A008", name: "brochure-trifold-v1.pdf", type: "Asset" },
approver: { id: "U003", login: "dporter", firstName: "Dave", lastName: "Porter" },
level: 1,
},
];
// === Mock Distribution Channels (from MMS Tech Architecture) ===
export const mockChannels: DistributionChannel[] = [
{ id: "CH01", name: "PIM (Stibo STEP)", category: "pim", icon: "database", description: "Product Information Management — sync product data & assets", status: "connected" },
{ id: "CH02", name: "Website (Unified Platform)", category: "web", icon: "globe", description: "MediaMarkt.es web storefront", status: "connected" },
{ id: "CH03", name: "Mobile App", category: "web", icon: "smartphone", description: "MediaMarkt mobile app (Unified Platform)", status: "connected" },
{ id: "CH04", name: "E-Commerce (Shop/PWA)", category: "web", icon: "shopping-cart", description: "Consumer web shop & progressive web app", status: "connected" },
{ id: "CH05", name: "Meta (Facebook & Instagram)", category: "social", icon: "facebook", description: "Publish to Facebook & Instagram feeds, stories, and ads", status: "connected" },
{ id: "CH06", name: "TikTok", category: "social", icon: "video", description: "Push creative assets to TikTok Ads Manager", status: "connected" },
{ id: "CH07", name: "LinkedIn", category: "social", icon: "linkedin", description: "Distribute to LinkedIn company page & ad campaigns", status: "available" },
{ id: "CH08", name: "Google Ads & Performance Max", category: "google", icon: "google", description: "Google Ads, Performance Max, CM360, YouTube", status: "connected" },
{ id: "CH09", name: "YouTube", category: "google", icon: "youtube", description: "Upload video assets to YouTube channels", status: "available" },
{ id: "CH10", name: "Electronic Shelf Labels (Pricer)", category: "instore", icon: "tag", description: "Push pricing & product images to in-store ESL", status: "connected" },
{ id: "CH11", name: "In-Store TV", category: "instore", icon: "monitor", description: "Digital signage content for in-store TV screens", status: "connected" },
{ id: "CH12", name: "In-Store Screens (Digital)", category: "instore", icon: "layout", description: "Interactive in-store display screens", status: "available" },
{ id: "CH13", name: "In-Store POP", category: "instore", icon: "printer", description: "Point-of-purchase materials & signage", status: "available" },
{ id: "CH14", name: "Print Fulfillment", category: "print", icon: "printer", description: "Flyers, catalogues, and print production", status: "connected" },
{ id: "CH15", name: "Customer Comms Hub", category: "advertising", icon: "mail", description: "Email, SMS, and push notifications (Unified Platform)", status: "connected" },
{ id: "CH16", name: "Programmatic (DV360 / Trade Desk)", category: "advertising", icon: "target", description: "Programmatic display & video advertising", status: "connected" },
{ id: "CH17", name: "Retail Media & Marketplace", category: "advertising", icon: "store", description: "Retail media network and marketplace listings", status: "coming_soon" },
];
// === Mock Distribution Jobs ===
export const mockDistributionJobs: DistributionJob[] = [
{ id: "DJ01", assetId: "A001", assetName: "hero-banner-spring-v3.pdf", channelId: "CH02", channelName: "Website (Unified Platform)", status: "completed", initiatedBy: "jsmith", initiatedAt: "2024-03-20T14:00:00Z", completedAt: "2024-03-20T14:02:30Z" },
{ id: "DJ02", assetId: "A001", assetName: "hero-banner-spring-v3.pdf", channelId: "CH05", channelName: "Meta (Facebook & Instagram)", status: "completed", initiatedBy: "jsmith", initiatedAt: "2024-03-20T14:05:00Z", completedAt: "2024-03-20T14:06:15Z" },
{ id: "DJ03", assetId: "A007", assetName: "logo-rebrand-primary.svg", channelId: "CH01", channelName: "PIM (Stibo STEP)", status: "processing", initiatedBy: "mwilson", initiatedAt: "2024-03-25T09:00:00Z" },
{ id: "DJ04", assetId: "A003", assetName: "packaging-front-v2.ai", channelId: "CH14", channelName: "Print Fulfillment", status: "queued", initiatedBy: "dporter", initiatedAt: "2024-03-22T10:30:00Z" },
{ id: "DJ05", assetId: "A004", assetName: "social-media-kit.zip", channelId: "CH06", channelName: "TikTok", status: "completed", initiatedBy: "mwilson", initiatedAt: "2024-03-18T16:00:00Z", completedAt: "2024-03-18T16:01:45Z" },
{ id: "DJ06", assetId: "A006", assetName: "booth-backdrop-3m.pdf", channelId: "CH11", channelName: "In-Store TV", status: "failed", initiatedBy: "jsmith", initiatedAt: "2024-03-22T11:00:00Z" },
{ id: "DJ07", assetId: "A007", assetName: "logo-rebrand-primary.svg", channelId: "CH08", channelName: "Google Ads & Performance Max", status: "processing", initiatedBy: "mwilson", initiatedAt: "2024-03-25T09:05:00Z" },
{ id: "DJ08", assetId: "A002", assetName: "product-shot-01.tif", channelId: "CH10", channelName: "Electronic Shelf Labels (Pricer)", status: "completed", initiatedBy: "dporter", initiatedAt: "2024-03-15T10:00:00Z", completedAt: "2024-03-15T10:00:45Z" },
];
// === Mock Processes ===
export const mockProcesses: ProcessMonitorEntry[] = [
{
id: "PR001",
name: "Preflight Check",
status: "COMPLETED",
progress: 100,
startDate: "2024-03-20T14:00:00Z",
endDate: "2024-03-20T14:02:00Z",
entity: { id: "A001", name: "hero-banner-spring-v3.pdf", type: "Asset" },
},
{
id: "PR002",
name: "Color Conversion",
status: "IN_PROGRESS",
progress: 65,
startDate: "2024-03-22T10:00:00Z",
entity: { id: "A003", name: "packaging-front-v2.ai", type: "Asset" },
},
{
id: "PR003",
name: "PDF Export",
status: "QUEUED",
progress: 0,
entity: { id: "A006", name: "booth-backdrop-3m.pdf", type: "Asset" },
},
];

View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

34
dalim-app/tsconfig.json Normal file
View file

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}

10815
docs/api_parsed.json Normal file

File diff suppressed because it is too large Load diff

675
docs/dalim-api-index.md Normal file
View file

@ -0,0 +1,675 @@
# Dalim ES FUSiON API — Full Capability Index
This file lists **all** GraphQL operations available in the Dalim ES FUSiON API.
For detailed docs (arguments, types, examples) on actively used endpoints, see `dalim-api-reference.md`.
**Total: 466 operations** — 173 Queries, 272 Mutations, 21 Subscriptions
## Authentication & System
### Queries
- `whoami` — No description → `a Whoami!`
- `serverInformation` — No description → `a ServerInformation!`
- `authentications` — No description → `[Authentication!]!`
- `clientApplications` — No description → `[ClientApplication!]!`
- `getCorsSetting` — No description → `a CorsSetting!`
- `getLogLevel` — Return the current log level → `a LogLevel!`
- `getLogQueries` — Return if the server logs queries → `a Boolean!`
- `getMaxQueryComplexity` — No description → `an Int!`
- `getMaxQuerySize` — No description → `an Int!`
- `getMaxTopLevelFieldCount` — No description → `an Int!`
- `queryStatistics` — Return the current query' statistics → `[QueryStatistic!]!`
- `servlets` — List all registered servlet's name → `[String!]`
- `openDialogueConnection` — Open a Dialogue connection to a document → `a DialogueConnection!`
- `closeDialogueConnection` — Close a Dialogue connection → `an ID!`
- `dialogueConnections` — List open connections optionally to a single Asset with id/revision. → `[DialogueConnection!]`
- `get2FAQRCode` — Return the Authenticator QR code → `a Stream`
- `getSecret2FAKey` — Return the Authenticator secret key → `a String!`
- `send2FAQRCodeByEmail` — send the Authenticator QR code to the user per email → `a Boolean`
- `myWebAuthnRegistrations` — No description → `a WebAuthnPagingResponse!`
- `webAuthnRegistrations` — List all Webauthn registration of all users visible by the logged one. → `a WebAuthnPagingResponse!`
- `userConnectionById` — No description → `[UserConnection!]`
- `userConnectionByClientId` — No description → `[UserConnection!]`
- `userConnections` — No description → `a UserConnectionPagingResponse`
### Mutations
- `connectAs` — Enable an Admin to connect to ES as another User. The response is identical to a standard login, a new AccessToken is... → `a JSON!`
- `disconnectAs` — When connected as another user, returns to the original login, the current token is revoked. The response is identica... → `a JSON!`
- `createAuthentication` — No description → `an Authentication!`
- `deleteAuthentication` — No description → `a Boolean!`
- `editAuthentication` — No description → `an Authentication!`
- `createClientApplication` — No description → `a ClientApplication!`
- `deleteClientApplication` — No description → `a Boolean!`
- `editClientApplication` — No description → `a ClientApplication!`
- `setCorsSetting` — No description → `a CorsSetting!`
- `setLogLevel` — Set the server log level → `a LogLevel!`
- `setLogQueries` — To enable or disable the log of queries → `a Boolean!`
- `setMaxQueryComplexity` — No description → `an Int!`
- `setMaxQuerySize` — No description → `an Int!`
- `setMaxTopLevelFieldCount` — No description → `an Int!`
- `reset2FAKey` — No description → `a Boolean!`
- `deleteMyWebAuthnRegistration` — No description → `a Boolean!`
- `deleteWebAuthnRegistration` — No description → `a Boolean!`
- `editMyWebAuthnRegistration` — No description → `a WebAuthnRegistration`
- `editWebAuthnRegistration` — No description → `a WebAuthnRegistration`
- `startWebAuthnRegistration` — No description → `a WebAuthOptions!`
- `finishWebAuthnRegistration` — No description → `a Boolean!`
- `revokeUserConnection` — No description → `a Boolean!`
### Subscriptions
- `authenticationChanged` — No description → `an AuthenticationEvent`
- `reIndexAll` — No description → `a ProgressEvent`
- `dummy` — No description → `a Boolean`
## Users & Profiles
### Queries
- `users` — Retrieve visible users by the current logged user → `a UserPagingResponse!`
- `userById` — Retrieve user by id → `[User!]`
- `userPreference` — No description → `[UserPreference]`
### Mutations
- `createUser` — Create an User in the specified Organization → `a User!`
- `changeUser` — No description → `a User!`
- `deleteUser` — Delete one or many Users → `a Boolean!`
- `editUser` — Edit an User → `a User!`
- `editMyUser` — No description → `a User!`
- `moveUser` — Move an existing User in the specified Organization → `a User!`
- `changeMyPassword` — No description → `a Boolean!`
- `changeMyProfile` — No description → `a JSON!`
- `resetUserPassword` — Generate temporary password for a user, if possible an email is sent to the user with that password. The password is ... → `a String!`
- `editUserPreference` — Edit user preferences → `[UserPreference!]!`
- `uploadAvatar` — Returns an UploadStatus! → `an UploadStatus!`
### Subscriptions
- `userChange` — No description → `a UserEvent`
- `whenUserChange` — Subscribe to the changes of the current user. → `an EntityEvent`
## Security Profiles
### Queries
- `securityProfiles` — Retrieve SecurityProfile by filter → `a SecurityProfilePagingResponse!`
- `securityProfileById` — Retrieve SecurityProfile by id → `[SecurityProfile!]`
- `securityRoles` — List all SecurityRoles → `[SecurityRoleDefinition!]!`
### Mutations
- `createAdminSecurityProfile` — No description → `a UserSecurityProfile`
- `createDefaultSecurityProfile` — No description → `a UserSecurityProfile`
- `createUserSecurityProfile` — No description → `a UserSecurityProfile`
- `createSecurityProfileMask` — No description → `a SecurityProfileMask`
- `deleteSecurityProfile` — No description → `a Boolean!`
- `editSecurityProfile` — to edit a profile → `a SecurityProfile`
- `duplicateSecurityProfile` — to duplicate a profile → `a SecurityProfile`
- `addProfileToUser` — Add one or many SecurityProfiles to one or many Users → `a Boolean!`
- `removeProfileFromUser` — Remove one or many SecurityProfiles from one or many Users → `a Boolean!`
- `putSecurityProperties` — Add security properties to a security profile (user or mask) → `[SecurityProfile!]!`
- `removeSecurityProperties` — Remove security properties from a security profile (user or mask) → `[SecurityProfile!]!`
- `putSecurityRoles` — Add a security roles to a user Security Profile → `[UserSecurityProfile!]!`
- `removeSecurityRoles` — Remove security roles from a user Security Profile → `[UserSecurityProfile!]!`
- `setSecurityRoles` — replace security roles list to a user Security Profile → `[UserSecurityProfile!]!`
## Groups & Roles
### Queries
- `groups` — Retrieve visible groups by the current logged user → `a GroupPagingResponse!`
- `groupById` — Retrieve group by id → `[Group!]`
- `roles` — Retrieve visible roles by the current logged user → `a RolePagingResponse!`
- `roleById` — Retrieve role by id → `[Role!]`
### Mutations
- `createGroup` — Create a Group in the specified Organization → `a Group!`
- `deleteGroup` — Delete one or many Groups that are not System Groups → `a Boolean!`
- `editGroup` — Edit a Group that is not a System Group → `a Group!`
- `addUserToGroup` — Add one or many Users to one or many Groups that are not System Groups. All the Users provided will be added to each ... → `a Boolean!`
- `removeUserFromGroup` — Remove one or many Users from one or many Groups that are not System Groups. All the Users provided will be removed f... → `a Boolean!`
- `createRole` — Create a Role → `a Role!`
- `deleteRole` — Delete one or many Role(s) → `a Boolean!`
- `editRole` — Edit a Role → `a Role!`
- `addRoleToUser` — Add one or many Role(s) to one or many User(s) → `a Boolean!`
- `removeRoleFromUser` — Remove one or many Role(s) from one or many User(s) → `a Boolean!`
## Organizations & Customers
### Queries
- `organizations` — Retrieve visible organizations by the current logged user → `an OrganizationPagingResponse!`
- `organizationById` — No description → `[Organization!]`
- `organizationsByFilter` — No description → `an OrganizationPagingResponse!`
- `customers` — Retrieve Customers by filter → `a CustomerPagingResponse!`
- `customerById` — Retrieve Customers by ID or [ID] → `[Customer!]`
### Mutations
- `createOrganization` — Create an Organization in the specified Organization → `an Organization!`
- `deleteOrganization` — Delete one or many Organizations → `a Boolean!`
- `editOrganization` — Edit an Organization → `an Organization!`
- `uploadOrganizationLogo` — Returns an UploadStatus! → `an UploadStatus!`
- `createCustomer` — Create a Customer → `a Customer!`
- `deleteCustomer` — Delete one or many Customer(s) → `a Boolean!`
- `editCustomer` — Edit a Customer → `a Customer!`
- `addCustomerToGroup` — **Add one or many Customers to one group. → `a Boolean!`
- `addCustomerToOrganization` — **Add one or many Customers to one Organization. → `a Boolean!`
- `removeCustomerFromGroup` — Remove one or many Customers from one Group. → `a Boolean!`
- `removeCustomerFromOrganization` — Remove one or many Customers from one Organization. → `a Boolean!`
## Projects
### Queries
- `projects` — Retrieve Projects by filter → `a ProjectPagingResponse!`
- `projectById` — Retrieve Projects by ID or [ID] → `[Project!]`
- `projectTemplates` — Retrieve Project Templates → `a ProjectTemplateResponse!`
- `projectTemplateById` — Retrieve Project Templates by ID or [ID] → `[ProjectTemplate!]`
- `projectFolderById` — Returns ProjectFolders by their IDs TODO Change security role → `[ProjectFolder!]`
- `participantsByRoleFilter` — Retrieve visible participants that can use the given role in production list → `[Participant!]`
### Mutations
- `createProject` — security role handle by the mutationFetcher itself To create a Project for a Customer with a name → `a Project!`
- `deleteProject` — To delete a Project. By default the project is moved to the trash → `a Boolean!`
- `renameProject` — To rename a Project with Id and newName → `a Project!`
- `duplicateProject` — To duplicate a project → `a Project!`
- `editProject` — To edit a Project → `[Project!]`
- `moveProjectToCustomer` — To move one or several project(s) to a customer → `[Project!]!`
- `createProjectFolder` — To create a ProjectFolder - Note: reusing iFoldersetup for now as iProjectFolderSetup would be identical → `a ProjectFolder!`
- `deleteProjectFolder` — To delete a ProjectFolder. By default the ProjectFolder is moved to the trash → `a Boolean!`
- `renameProjectFolder` — To rename a ProjectFolder → `a ProjectFolder!`
- `editProjectFolder` — To edit a ProjectFolder → `[ProjectFolder!]`
- `moveProjectFolder` — To move a ProjectFolder - TODO using container as return for testing. Discuss long term return type. → `[Container!]!`
- `createProjectTemplate` — To create a ProjectTemplate → `a ProjectTemplate!`
- `deleteProjectTemplate` — To delete a ProjectTemplate → `a Boolean!`
- `editProjectTemplate` — To edit a ProjectTemplate → `[ProjectTemplate!]`
- `duplicateProjectTemplate` — To duplicate a ProjectTemplate → `a ProjectTemplate!`
- `createFolderFromProject` — To create a Folder from a Project in the selected Folder → `a Folder!`
- `createProjectFromFolder` — To create a project from an existing filesystem Folder → `a Project!`
### Subscriptions
- `projectChange` — No description → `a ProjectEvent`
- `projectFolderChange` — No description → `a ProjectFolderEvent`
## Folders
### Queries
- `folders` — Returns the paginated list of top-level folders.Use triggerMonitoring to override volume monitorOnBrowse setting → `a FolderPagingResponse!`
- `folderById` — Retrieve Folders by ID or [ID]. Use triggerMonitoring to override volume monitorOnBrowse setting, determining whether... → `[Folder!]`
### Mutations
- `createFolder` — To create a Folder → `a Folder!`
- `deleteFolder` — To delete a Folder. By default the Folder is moved to the trash → `a Boolean!`
- `renameFolder` — To rename a Folder → `a Folder!`
- `moveFolder` — To move a Folder - TODO Return type can't be folder anymore → `[Folder!]!`
- `editFolder` — To edit a Folder → `[Folder!]`
### Subscriptions
- `folderChange` — No description → `a FolderEvent`
## Assets
### Queries
- `assets` — Retrieve Assets by filter → `an AssetPagingResponse!`
- `assetById` — Retrieve Assets by ID or [ID] → `[Asset!]`
- `downloadAsset` — Stream an Asset → `a Stream`
- `downloadAssetWithNotes` — Download one Asset or a .zip of several Assets with notes translated to PDF annotations → `a Stream`
- `streamAttachment` — No description → `a Stream`
- `streamFile` — Stream a subFile of an Asset → `a Stream`
### Mutations
- `createAsset` — To create an Asset. → `an AssetCreationStatus!`
- `deleteAsset` — To delete an Asset. By default the Asset is moved to the trash → `a Boolean!`
- `renameAsset` — Not yet implemented : To rename an Asset with Id newName is used to rename the asset → `an Asset!`
- `moveAsset` — Move one or several Asset(s) into a Folder or a Project → `a Boolean!`
- `editAsset` — To edit an Asset → `[Asset!]`
- `checkOutAsset` — To checkOut the Asset(s) with Id or Ids → `[Asset!]`
- `checkInAsset` — To checkin a new version of an Asset → `an Asset!`
- `cancelCheckOutAsset` — To cancel the checkout of an Asset → `[Asset!]`
- `copyAssetToFolder` — Duplicate an asset. WARNING It is not currently possible to copy an asset between two ProjectFolders in the same Proj... → `[Asset!]!`
- `createAssetAlias` — To create an Asset alias where 'id' is the Asset ID and 'to' defines a project or a project subfolder ID → `a Boolean!`
- `removeAssetAlias` — To remove an Asset alias where 'id' is the Asset ID and 'from' defines a project or a project subfold... → `a Boolean!`
- `mergeAsset` — Merge Assets with their revisions → `an Asset!`
- `reprocessAsset` — Reprocess an asset's workflow → `[Process!]!`
- `editRevision` — Edit revision in an asset → `an Asset!`
- `editInk` — This is to edit the opacity of the inks of a Asset/Revision or Media → `a Media!`
- `uploadFile` — No description → `a Boolean!`
- `uploadOn` — to upload on a specific Asset : temporary → `an Asset`
- `uploadOnMilestone` — To upload on a Milestone → `an UploadStatus`
### Subscriptions
- `assetChange` — No description → `an AssetEvent`
- `mediaChange` — No description → `a MediaEvent`
## Collections
### Queries
- `collections` — Returns the paginated list of top-level collections. → `a CollectionPagingResponse!`
- `collectionById` — Retrieve Collections by ID or [ID] → `[Collection!]`
- `anyCollections` — Returns the paginated list of any top-level collections (Smart Collections and Collections). → `an AnyCollectionPagingResponse!`
- `smartCollections` — Returns the paginated list of smart collections. → `a SmartCollectionPagingResponse!`
- `smartCollectionById` — Retrieve Smart Collection by ID or [ID] → `[SmartCollection!]`
### Mutations
- `createCollection` — To create a Collection → `a Collection!`
- `deleteCollection` — To delete a Collection. By default the Collection is moved to the trash → `a Boolean!`
- `renameCollection` — To rename a Collection → `a Collection!`
- `editCollection` — To edit a Collection → `[Collection!]`
- `addObjectToCollection` — To add an object in a Collection → `a Boolean!`
- `removeObjectFromCollection` — To remove an object from a Collection → `a Boolean!`
- `moveCollection` — To move a Collection → `[Collection!]!`
- `moveObjectFromCollectionToCollection` — To move an object from one collection to another one → `a Boolean!`
- `createSmartCollection` — To create a SmartCollection → `a SmartCollection!`
- `deleteSmartCollection` — To delete a SmartCollection. By default the SmartCollection is moved to the trash → `a Boolean!`
- `editSmartCollection` — To edit a SmartCollection → `[SmartCollection!]`
- `renameSmartCollection` — To rename a SmartCollection → `a SmartCollection!`
### Subscriptions
- `collectionChange` — No description → `a CollectionEvent`
- `smartCollectionChange` — No description → `a SmartCollectionEvent`
## Search & Filters
### Queries
- `search` — No description → `a SearchResponse`
- `searchBySmartCollection` — No description → `a SearchResponse`
- `entityByPath` — No description → `[EntityPath!]`
- `dumpText` — Dump the text contained in the document → `[TextPage!]`
- `namedSearchFilters` — No description → `[NamedSearchFilter!]!`
- `namedSearchFilterById` — No description → `[NamedSearchFilter!]!`
- `namedSearchFilterByName` — No description → `[NamedSearchFilter!]!`
### Mutations
- `createNamedSearchFilter` — No description → `a NamedSearchFilter!`
- `editNamedSearchFilter` — No description → `a NamedSearchFilter!`
## Approvals
### Queries
- `approvals` — Retrieve objects to approve by the current logged user → `an ApprovalPagingResponse!`
- `approvalsByUser` — Retrieve objects to approve for a specific user → `an ApprovalPagingResponse!`
### Mutations
- `approve` — No description → `a Boolean!`
- `approveObject` — No description → `a Boolean!`
- `reject` — No description → `a Boolean!`
- `rejectObject` — No description → `a Boolean!`
## Workflows & Processes
### Queries
- `workflows` — Retrieve Workflows by filter → `a WorkflowPagingResponse!`
- `workflowsById` — Retrieve Workflows by ID or [ID] → `[Workflow!]`
- `workflowsByName` — Retrieve Workflows by name or [name] When revision is not defined returns the current revision, when revision is 0 re... → `[Workflow!]`
- `workflowsByEntity` — No description → `[Workflow!]`
- `workflowEngine` — Retrieve Workflow Engine infos → `a WorkflowEngine!`
- `processes` — Retrieve Processes by filter → `a ProcessPagingResponse!`
- `processById` — Retrieve Processes by ID or [ID] → `[Process!]`
- `processMonitoring` — Workflow activity → `a MonitoringPagingResponse!`
### Mutations
- `createWorkflow` — Worflow creation → `a Workflow!`
- `deleteWorkflow` — Workflow deletion : to delete a workflow with entityId 0 → `a Boolean!`
- `editWorkflow` — createWorkflowRevision(name:String!): Workflow! @security(role:ADMIN_WORKFLOW) To define the current revision of a Wo... → `a Workflow!`
- `duplicateWorkflow` — deleteWorkflowRevision(name:String! revision:Int!):Boolean! @security(role:ADMIN_WORKFLOW) Workflow duplication in ca... → `a Workflow!`
- `editWorkflowEngineCapacity` — Edit capacity of a Workflow Engine → `a Boolean!`
- `cancelProcess` — To cancel a workflow → `[Process!]!`
- `deleteProcess` — To delete a Process → `a Boolean!`
- `changeProcessPriority` — To change the priority of a workflow → `[Process!]!`
- `startProcess` — Start a workflow on an object → `[Process!]!`
- `startFileProcess` — No description → `a Process!`
- `startURLProcess` — To start a workflow without Object → `a Process!`
- `submitURLProcess` — No description → `a Boolean!`
- `restartProcess` — Restart a workflow on an object from an activity → `[Process!]!`
- `restartActivity` — Restart a workflow on an object from an activity → `[Process!]!`
### Subscriptions
- `processChanged` — No description → `a ProcessEvent!`
## Notes & Annotations
### Queries
- `getNotes` — Returns [Note]! → `[Note]!`
- `noteReport`**Generate a Report for one or several Asset(s) **`a Stream`
- `annotationStatuses` — No description → `[AnnotationStatus!]`
- `annotationStatusById` — No description → `[AnnotationStatus!]`
- `proofReadingMarks` — Returns a ProofReadingMarkPagingResponse! → `a ProofReadingMarkPagingResponse!`
- `proofReadingMarksById` — Returns [ProofReadingMark!] → `[ProofReadingMark!]`
- `myProofReadingMarks` — Returns a ProofReadingMarkPagingResponse! → `a ProofReadingMarkPagingResponse!`
- `myProofReadingMarksById` — Returns [ProofReadingMark!] → `[ProofReadingMark!]`
### Mutations
- `createNote` — Returns [Note!]! → `[Note!]!`
- `deleteNote` — Returns a Boolean! → `a Boolean!`
- `editNote` — Returns a Note! → `a Note!`
- `replyToNote` — rank : The rank of the reply. This allows to insert a reply at a certain rank, provided a reply with this rank alread... → `a Note!`
- `editReplyOfNote` — Returns a Note! → `a Note!`
- `deleteReplyOfNote` — Returns a Note! → `a Note!`
- `addNoteAttachment` — If a rank is specified the attachment will be added to the reply of the given rank If an attachment ID is specified, ... → `a Note!`
- `removeNoteAttachment` — Returns a Boolean! → `a Boolean!`
- `createAnnotationStatus` — Returns an AnnotationStatus! → `an AnnotationStatus!`
- `deleteAnnotationStatus` — Returns a Boolean → `a Boolean`
- `editAnnotationStatus` — Returns an AnnotationStatus! → `an AnnotationStatus!`
- `createGlobalProofReadingMark` — Returns a Boolean! → `a Boolean!`
- `createPrivateProofReadingMark` — Returns a Boolean! → `a Boolean!`
- `deleteAnyProofReadingMark` — Returns a Boolean! → `a Boolean!`
- `deleteMyProofReadingMark` — Returns a Boolean! → `a Boolean!`
- `editGlobalProofReadingMark` — Returns a Boolean! → `a Boolean!`
- `editPrivateProofReadingmark` — Returns a Boolean! → `a Boolean!`
### Subscriptions
- `noteChange` — No description → `an OldNoteEvent`
- `whenNoteChange` — No description → `a NoteEvent`
- `proofReadingMarkChange` — No description → `a ProofReadingMarkEvent`
## Notifications
### Queries
- `notifications` — Retrieve the notifications of a given type → `[Notification!]`
- `userNotifications` — Retrieve the notifications of the current user → `[Notification!]`
- `notificationTemplates` — No description → `a NotificationTemplatePagingResponse!`
- `notificationTemplateById` — No description → `[NotificationTemplate!]`
### Mutations
- `createNotificationTemplate` — No description → `a NotificationTemplate!`
- `deleteNotificationTemplate` — The default ones cannot be deleted → `a Boolean!`
- `editNotificationTemplate` — No description → `a NotificationTemplate!`
- `editNotifications` — No description → `[Notification!]`
- `disableNotifications` — for the current user → `a Boolean!`
- `enableNotifications` — No description → `a Boolean!`
- `stopStartNotificationSender` — No description → `a Boolean`
- `markAsSeen` — Returns a Boolean! → `a Boolean!`
- `resendMail` — not yet implemented → `a Boolean!`
### Subscriptions
- `notificationSender` — No description → `a SenderEvent`
## Sharing
### Queries
- `getSharing` — to retrieve the current sharing when logged in from a sharing key → `a Sharing`
- `sharingById` — No description → `[Sharing!]`
- `sharingByUser` — here Name or ID for the users? → `a SharingResponse!`
- `sharings` — No description → `a SharingResponse!`
- `mySharing` — No description → `a SharingResponse!`
### Mutations
- `deleteSharing` — No description → `a Boolean!`
- `deleteMySharing` — No description → `a Boolean!`
- `editSharing` — No description → `a Sharing!`
- `editMySharing` — No description → `a Sharing!`
- `share` — No description → `[Sharing!]!`
## Email & Output Channels
### Queries
- `emailTemplates` — Returns an EmailTemplatePagingResponse! → `an EmailTemplatePagingResponse!`
- `emailTemplateById` — No description → `[EmailTemplate!]`
- `outputChannelGroups` — Get output channel groups → `an OutputChannelGroupPagingResponse!`
- `outputChannelGroupById` — Get output channel group(s) based on ID → `[OutputChannelGroup!]!`
- `outputChannelById` — Get output channel(s) based on ID → `[OutputChannel!]!`
### Mutations
- `createEmailTemplate` — No description → `an EmailTemplate!`
- `deleteEmailTemplate` — No description → `a Boolean!`
- `editEmailTemplate` — No description → `an EmailTemplate!`
- `createOutputChannelGroup` — Create an output channel group Note that it is possible to create an empty channel group → `an OutputChannelGroup!`
- `deleteOutputChannelGroup` — Delete one or more output channel groups Will also delete any output channels in the group → `a Boolean!`
- `editOutputChannelGroup` — Edit output channel groups → `[OutputChannelGroup!]!`
- `createEmailOutputChannel` — Create an email output channel → `an EmailOutputChannel!`
- `deleteEmailOutputChannel` — Delete one or more email output channels → `a Boolean!`
- `editEmailOutputChannel` — Edit email output channels → `[EmailOutputChannel!]!`
- `createFileOutputChannel` — Create a file output channel → `a FileOutputChannel!`
- `deleteFileOutputChannel` — Delete one or more file output channels → `a Boolean!`
- `editFileOutputChannel` — Edit file output channels → `[FileOutputChannel!]!`
## Input Channels & Rules
### Queries
- `inputChannels` — No description → `an InputChannelPagingResponse!`
- `inputChannelById` — No description → `[InputChannel!]`
- `inputRuleSets` — No description → `an InputRuleSetPagingResponse!`
- `inputRuleSetById` — No description → `[InputRuleSet!]`
### Mutations
- `createInputChannel` — No description → `an InputChannel!`
- `deleteInputChannel` — No description → `a Boolean!`
- `editInputChannel` — Edit an Input channelid Id of the Input channel to editpipes If pipes is given, the current pipes will be replaced by... → `an InputChannel!`
- `createInputRuleSet` — No description → `an InputRuleSet!`
- `deleteInputRuleSet` — No description → `a Boolean!`
- `editInputRuleSet` — No description → `an InputRuleSet!`
## Metadata
### Queries
- `metadataDefinitionById` — No description → `[MetadataDefinition!]`
- `metadataDefinitionByRef` — No description → `[MetadataDefinition!]`
- `nameSpaceDefinitions` — No description → `[MetadataNameSpace!]`
- `nameSpaceDefinitionById` — No description → `[MetadataNameSpace!]`
- `nameSpaceDefinitionByPrefix` — No description → `[MetadataNameSpace!]`
- `nameSpaceDefinitionByURI` — No description → `[MetadataNameSpace!]`
- `getProperties` — Read all configuration properties of a particular category → `[ConfigProperty!]!`
- `getProperty` — Read a configuration property → `a ConfigProperty`
### Mutations
- `createMetadataDefinition` — To create a metadata definition → `a MetadataDefinition`
- `deleteMetadataDefinition` — To delete a metadata definition → `a Boolean!`
- `editMetadataDefinition` — To edit a metadata definition → `a MetadataDefinition`
- `renameMetadataDefinition` — To rename a metadata definition → `a MetadataDefinition`
- `createMetadataNameSpace` — To create a metadata nameSpace → `a MetadataNameSpace`
- `deleteMetadataNameSpace` — To delete a nameSpace definition → `a Boolean!`
- `editMetadataNameSpace` — To edit a metadata nameSpace → `a MetadataNameSpace`
- `setProperties` — To set propert-y-ies → `[ConfigProperty!]`
- `setContent` — replace the content of an asset the Asset must be checked out → `an Asset!`
- `deleteProperty` — To delete a property → `a ConfigProperty!`
## Thesaurus & Taxonomy
### Queries
- `thesaurus` — No description → `[Thesaurus!]!`
- `thesaurusById` — No description → `[Thesaurus!]`
- `thesaurusByLabel` — No description → `[Thesaurus!]`
- `thesaurusByURI` — No description → `[Thesaurus!]`
- `synSetById` — No description → `[SynSet!]`
- `synSetByLabel` — No description → `[SynSet!]`
- `synSetByURI` — No description → `[SynSet!]`
### Mutations
- `createThesaurus` — No description → `a Thesaurus!`
- `deleteThesaurus` — No description → `a Boolean!`
- `createSynSet` — No description → `a SynSet!`
- `deleteSynSet` — No description → `a Boolean!`
- `editSynSet` — No description → `a SynSet`
- `reactivateSynSet` — No description → `a Boolean!`
- `retireSynSet` — No description → `a Boolean!`
## Volumes & Hosts
### Queries
- `volumes` — List all volumes → `[Volume!]!`
- `volumeById` — Get volume(s) based on ID → `[Volume!]!`
- `checkVolumeConnectivity` — check connectivity of a volume to it's storage location or server → `a Boolean!`
- `hosts` — List all hosts → `[Host!]!`
- `hostById` — Get host(s) based on ID → `[Host!]!`
- `hostFoldersByPath`**List folders in a host **`[String!]!`
### Mutations
- `createVolume` — Create a Volume → `a Volume!`
- `createFileSystemVolume` — Create a File System Volume → `a FileSystemVolume!`
- `deleteVolume` — Delete a Volume → `a Boolean!`
- `editVolume` — Edit a Volume → `[Volume!]!`
- `editFileSystemVolume` — Edit a File System Volume → `[FileSystemVolume!]!`
- `updateFileSystemVolumeDiskId` — Update a File System Volume Disk UUID → `[FileSystemVolume!]!`
- `deleteHost` — Delete a Host deprecated → `a Boolean!`
- `editHost` — Edit a Host deprecated → `a Host!`
## Color Management
### Queries
- `colorSpaces` — No description → `a ColorSpacePagingResponse!`
- `colorSpaceById` — No description → `[ColorSpace!]`
- `iccProfiles` — No description → `an IccProfilePagingResponse!`
- `iccProfileById` — No description → `[IccProfile!]`
- `iccProfileByName` — No description → `an IccProfile!`
- `inks` — No description → `an InkPagingResponse!`
- `inkById` — No description → `[Ink!]`
- `inkByName` — No description → `an Ink!`
- `inkCoverage` — Compute the ink coverage of a portion of a document defined by parameters of input iInkCoverageParams The response is... → `a Stream`
- `viewingConditions` — No description → `a ViewingConditionPagingResponse!`
- `viewingConditionById` — No description → `[ViewingCondition!]`
- `editIccContext` — Setup ICC context → `a Boolean!`
- `densitometer` — Read the color value at a certain position → `a DensitometerValues`
- `gamutCheck` — Compute the area where the color are out of gamut according to the monitor or to a simulation profile of a portion of... → `a Stream`
- `gamutWarning` — Return true if the document is viewed out of gamut → `a Boolean!`
### Mutations
- `createColorSpace` — No description → `a ColorSpace!`
- `deleteColorSpace` — No description → `a Boolean!`
- `editColorSpace` — No description → `a ColorSpace!`
- `deleteIccProfile` — No description → `a Boolean`
- `uploadIccProfile` — No description → `an UploadStatus!`
- `createViewingCondition` — No description → `a ViewingCondition!`
- `deleteViewingCondition` — No description → `a Boolean!`
- `editViewingCondition` — No description → `a ViewingCondition!`
## Layouts
### Queries
- `layouts` — Retrieve Layouts → `a LayoutPagingResponse!`
- `layoutById` — Retrieve Layout by ID or [ID] → `[Layout!]`
### Mutations
- `createLayout` — Create a layout → `a Layout!`
- `deleteLayout` — Delete a layout → `a Boolean!`
- `editLayout` — Edit a layout → `a Layout!`
## User Actions
### Queries
- `userActions` — List all user actions → `[UserAction!]!`
- `userActionById` — Get user action(s) based on ID → `[UserAction!]!`
- `userActionIconNames`** List the names of all uploaded user action icons **`[String!]!`
- `userActionInstancesById`** Get user action instance(s) by id **`[UserActionInstance!]!`
- `userActionInstancesByUser`** Get user action instance(s) executed by the specified user** → `[UserActionInstance!]!`
### Mutations
- `createUserAction`** Create a user action** → `a UserAction!`
- `deleteUserAction`** Delete one or more user actions** → `a Boolean!`
- `editUserAction`** Edit one or more user actions** → `[UserAction!]!`
- `clearUserActionInstancesById`** Delete running or completed user action(s) specified by ID** → `a Boolean!`
- `clearUserActionInstancesByUser`** Delete running or completed user action(s) linked to the specified user** → `a Boolean!`
- `startUserAction` — Start a user action on one or more objects → `a Boolean!`
- `uploadUserActionIcon`** Upload a user action icon** → `an UploadStatus!`
### Subscriptions
- `userActionChange` — No description → `an OldUserActionEvent`
- `whenUserActionChange` — Events for UserActions triggered by the current user → `a UserActionEvent`
## UI & Files
### Queries
- `getUIFiles` — No description → `[UIProjectFile!]`
- `getUIFileContent` — No description → `a String`
- `getUIFilesByFilter` — No description → `[UIProjectFile!]`
- `getUIProjects` — No description → `[UIProject!]`
- `getUIProjectFiles` — No description → `[UIProjectFile!]`
- `getUIProjectsByFilter` — No description → `[UIProject!]`
- `getUIProjectsByName` — No description → `an UIProject`
- `getUIProjectsByType` — No description → `[UIProject!]`
- `streamUIFileContent` — No description → `a Stream`
- `getFileContent` — -------- Deprecated --------- → `a String`
### Mutations
- `createUIFile` — No description → `an UIProjectFile!`
- `createAndUploadUIFile` — No description → `a Boolean!`
- `deleteUIFile` — No description → `a Boolean`
- `editUIFile` — No description → `an UIProjectFile!`
- `saveUIFile` — No description → `a Boolean!`
- `uploadUIFile` — No description → `a Boolean!`
- `createUIFolder` — No description → `an UIProjectFile`
- `deleteUIFolder` — No description → `a Boolean`
- `createUIProject` — No description → `an UIProject`
- `createUIProjectWithType` — No description → `an UIProject`
- `deleteUIProject` — No description → `a Boolean`
- `editUIProject` — No description → `an UIProject`
- `saveFile` — -------- Deprecated --------- → `a Boolean!`
## Preflighting & Rasterizing
### Queries
- `preflightReport`**Generate a Preflight report for one or several Asset(s) or Media(s) **`a Stream`
- `streamPreflightPreview` — Retrieve the preview of a given page of the preflight report → `a Stream`
- `rasterize` — Rendering of a portion of a document defined by parameters of input iRasterizeParams The response is a jpeg image (Wa... → `a Stream`
- `readBarCode` — Read a Barcode in the area defined by it's coordinates (in pt) → `[BarCode!]!`
## Import / Export
### Queries
- `importedFiles` — **Get the list of files that have been imported → `[ImportedFile!]!`
- `getImportConflicts`** Retrieve the objects to import that conflicts with existing one for a given ImportedFile → `[ImportedInfo!]`
- `getImportInfo`** Retrieve the objects to import for a given ImportedFile → `[ImportedInfo!]`
- `exportData` — **Export the given object(s) in an .es file → `a Stream`
- `getSchemaExtensions` — No description → `[SchemaExtension!]`
### Mutations
- `importData` — **Import the given file → `a Boolean`
- `uploadImportFile` — **Upload the given file in the imported files → `an UploadStatus!`
- `createSchemaExtension` — No description → `a SchemaExtension!`
- `deleteSchemaExtension` — No description → `a Boolean!`
- `editSchemaExtension` — No description → `a SchemaExtension!`
## Events & Logging
### Queries
- `getEventLogs` — No description → `a LogResponse`
### Subscriptions
- `whenObjectChange` — ParentId only works with CREATE_OBJECT EntityEventType and not on Media and User EntityTypes → `an EntityEvent`
- `whenStepStatusChange` — No description → `a StepEvent`
## Relations
### Mutations
- `createRelation` — To create Relations between 1 source Entity and one or more target Entities → `[Relation!]`
- `deleteRelation` — To delete Relations by there ID(s) → `a Boolean!`
- `deleteRelationFromObject` — To delete Relations from there target and/or sources one of source or target is mandatory → `a Boolean!`
- `editRelation` — To edit Relation → `[Relation!]`
## Reports
### Queries
- `reports` — No description → `[Report!]`
- `getReportURL` — No description → `a String`
## Trash
### Queries
- `trashedObjects` — No description → `a TrashablePagingResponse!`
### Mutations
- `deleteAllTrashedObjects` — Delete all objects that have been trashed with the current user → `a Boolean!`
- `deleteObject` — Generic delete object → `a Boolean!`
- `trashObject` — No description → `a Boolean!`
- `unTrashObject` — Untrash object(s) → `a Boolean!`
- `unTrashAllObjects` — Untrash all objects that have been trashed with the current user → `a Boolean!`
## Milestones
### Mutations
- `createMilestone` — To create a Milestone → `an Activity!`
## Activities
### Queries
- `activities` — List of topLevel activities → `[Activity!]`
- `activityById` — Activity by Id → `[Activity!]`
- `activityIconById` — No description → `[ActivityIcon!]`
- `activityIcons` — Retrieve Activity Icon → `an ActivityIconPagingResponse!`
- `activityPresets` — List of activity presets → `[Activity!]`
- `activityVariables` — Retrieve variables names depending on WorkflowableTypeName → `[String]`
### Mutations
- `deleteActivity` — To delete an activity → `a Boolean!`
- `deleteActivityIcon` — delete an activity icon → `a Boolean`
- `deleteActivityPreset` — To delete a preset → `a Boolean!`
- `duplicateActivity` — To duplicate Top Level Activity → `an Activity!`
- `createCustomActivity` — Custom activity creation → `an Activity!`
- `deleteCustomActivity` — Custom activity deletion → `a Boolean`
- `editActivity` — To edit Top Level Activity toRemove: true will throw an Exception → `an Activity!`
- `editActivityPreset` — To edit activity presets creates it if it doesn't exist → `an Activity!`
- `editCustomActivity` — Custom activity edition → `an Activity!`
- `editActivityEngineCapacity` — Edit capacity of an ActivityEngine in a specific Workflow Engine → `a Boolean!`
- `editActivityEngineTemplate` — Edit an ActivityEngineTemplate in a specific Workflow Engine → `a Boolean!`
- `uploadActivityIcon` — upload an activity icon → `an UploadStatus!`

955
docs/dalim-api-reference.md Normal file
View file

@ -0,0 +1,955 @@
# Dalim ES FUSiON API — Active Endpoint Reference
Detailed documentation for the endpoints actively used in this project.
For a full list of all 466 available operations, see `dalim-api-index.md`.
---
## Authentication
The API uses OAuth2 with HMAC SHA256 signing.
**Token endpoint:** `https://{HOST}/ES/api/oauth/token`
**GraphQL endpoint:** `https://{HOST}/ES/api/graphql`
**Get a token:**
```python
token_data = {
"grant_type": "password",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"username": USERNAME,
"password": PASSWORD
}
response = requests.post(TOKEN_URL, data=token_data)
access_token = response.json()["access_token"]
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
```
**Dependency chain** (must create in this order):
Security Profiles → Users → Projects → Assets
---
## System & Auth
### connectAs
**Type:** Mutation
**Returns:** `a JSON!`
Enable an Admin to connect to ES as another User. The response is identical to a standard login, a new AccessToken is received.
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `login` | `String!` | Yes | login of the user |
| `withSecurityProfile` | `String` | No | name of a securityProfile |
| `createIfNotExists` | `Boolean` | No | Auto-created user if not exists |
**Example Query:**
```graphql
mutation connectAs( $login: String!, $withSecurityProfile: String, $createIfNotExists: Boolean ) { connectAs( login: $login, withSecurityProfile: $withSecurityProfile, createIfNotExists: $createIfNotExists ) }
```
**Example Variables:**
```json
{ "login": "abc123", "withSecurityProfile": "abc123", "createIfNotExists": true }
```
**Example Response:**
```json
{"data": {"connectAs": {}}}
```
---
### disconnectAs
**Type:** Mutation
**Returns:** `a JSON!`
When connected as another user, returns to the original login, the current token is revoked. The response is identical to a standard login, a new AccessToken is received. If the AccessToken does not correspond to a user connected as, then nothing append, the current token is still valid.
**Example Query:**
```graphql
mutation disconnectAs { disconnectAs }
```
**Example Response:**
```json
{"data": {"disconnectAs": {}}}
```
---
### whoami
**Type:** Query
**Returns:** `a Whoami!`
**Example Query:**
```graphql
query whoami { whoami { id user { ...UserFragment } securityProfile { ...UserSecurityProfileFragment } } }
```
**Example Response:**
```json
{ "data": { "whoami": { "id": "4", "user": User, "securityProfile": UserSecurityProfile } } }
```
---
### serverInformation
**Type:** Query
**Returns:** `a ServerInformation!`
**Example Query:**
```graphql
query serverInformation { serverInformation { guiClientId authorizationURL accessTokenURL authenticationKeys } }
```
**Example Response:**
```json
{ "data": { "serverInformation": { "guiClientId": "abc123", "authorizationURL": "http://www.test.com/", "accessTokenURL": "http://www.test.com/", "authenticationKeys": ["4"] } } }
```
---
## Users
### users
**Type:** Query
**Returns:** `a UserPagingResponse!`
Retrieve visible users by the current logged user
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iFilter` | No | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
| `orderBy` | `iOrderBy` | No | Default = {property : "id", direction : ASC} |
**Example Query:**
```graphql
query users( $filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy ) { users( filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy ) { lowerCursor upperCursor hasMoreItems objectList { ...UserFragment } } }
```
**Example Variables:**
```json
{ "filter": iFilter, "limit": 123, "cursor": 4, "orderBy": {"property": "id", "direction": "ASC"} }
```
**Example Response:**
```json
{ "data": { "users": { "lowerCursor": "4", "upperCursor": "4", "hasMoreItems": true, "objectList": [User] } } }
```
---
### userById
**Type:** Query
**Returns:** `[User!]`
Retrieve user by id
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | — |
**Example Query:**
```graphql
query userById($id: [ID!]!) { userById(id: $id) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } login userCanLog lang dateFormat unitResolution unitLength color image use2FA channel2FA sessionTimeout workCapacity workCapacityUnit firstName lastName email title company phone phone2 homePhone fax mobilePhone department address { ...AddressFragment } organization { ...OrganizationFragment } groups { ...GroupFragment } roles { ...RoleFragment } defaultProfile { ...UserSecurityProfileFragment } availableProfiles { ...UserSecurityProfileFragment } notifications { ...NotificationFragment } } }
```
**Example Variables:**
```json
{"id": ["4"]}
```
**Example Response:**
```json
{ "data": { "userById": [ { "id": "4", "name": "abc123", "description": "xyz789", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "login": "abc123", "userCanLog": true, "lang": "ar", "dateFormat": "abc123", "unitResolution": "xyz789", "unitLength": "xyz789", "color": "xyz789", "image": "xyz789", "use2FA": true, "channel2FA": "AUTHENTICATOR", "sessionTimeout": 123, "workCapacity": "abc123", "workCapacityUnit": "xyz789", "firstName": "xyz789", "lastName": "abc123", "email": "abc123", "title": "abc123", "company": "xyz789", "phone": "abc123", "phone2": "xyz789", "homePhone": "xyz789", "fax": "abc123", "mobilePhone": "xyz789", "departm
... (truncated)
```
---
### createUser
**Type:** Mutation
**Returns:** `a User!`
Create an User in the specified Organization
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | `String!` | Yes | — |
| `in` | `ID!` | Yes | — |
| `setup` | `iCreateUserSetup` | No | — |
**Example Query:**
```graphql
mutation createUser( $name: String!, $in: ID!, $setup: iCreateUserSetup ) { createUser( name: $name, in: $in, setup: $setup ) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } login userCanLog lang dateFormat unitResolution unitLength color image use2FA channel2FA sessionTimeout workCapacity workCapacityUnit firstName lastName email title company phone phone2 homePhone fax mobilePhone department address { ...AddressFragment } organization { ...OrganizationFragment } groups { ...GroupFragment } roles { ...RoleFragment } defaultProfile { ...UserSecurityProfileFragment } availableProfiles { ...UserSecurityProfileFragment } notifications { ...NotificationFragment } } }
```
**Example Variables:**
```json
{ "name": "abc123", "in": 4, "setup": iCreateUserSetup }
```
**Example Response:**
```json
{ "data": { "createUser": { "id": 4, "name": "abc123", "description": "xyz789", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "login": "xyz789", "userCanLog": false, "lang": "ar", "dateFormat": "abc123", "unitResolution": "xyz789", "unitLength": "xyz789", "color": "xyz789", "image": "abc123", "use2FA": true, "channel2FA": "AUTHENTICATOR", "sessionTimeout": 123, "workCapacity": "xyz789", "workCapacityUnit": "xyz789", "firstName": "xyz789", "lastName": "xyz789", "email": "abc123", "title": "xyz789", "company": "xyz789", "phone": "xyz789", "phone2": "xyz789", "homePhone": "abc123", "fax": "xyz789", "mobilePhone": "abc123", "departme
... (truncated)
```
---
### changeUser
**Type:** Mutation
**Returns:** `a User!`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | `String!` | Yes | — |
**Example Query:**
```graphql
mutation changeUser($name: String!) { changeUser(name: $name) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } login userCanLog lang dateFormat unitResolution unitLength color image use2FA channel2FA sessionTimeout workCapacity workCapacityUnit firstName lastName email title company phone phone2 homePhone fax mobilePhone department address { ...AddressFragment } organization { ...OrganizationFragment } groups { ...GroupFragment } roles { ...RoleFragment } defaultProfile { ...UserSecurityProfileFragment } availableProfiles { ...UserSecurityProfileFragment } notifications { ...NotificationFragment } } }
```
**Example Variables:**
```json
{"name": "abc123"}
```
**Example Response:**
```json
{ "data": { "changeUser": { "id": "4", "name": "abc123", "description": "abc123", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "login": "xyz789", "userCanLog": true, "lang": "ar", "dateFormat": "xyz789", "unitResolution": "abc123", "unitLength": "abc123", "color": "xyz789", "image": "abc123", "use2FA": true, "channel2FA": "AUTHENTICATOR", "sessionTimeout": 123, "workCapacity": "xyz789", "workCapacityUnit": "xyz789", "firstName": "abc123", "lastName": "xyz789", "email": "xyz789", "title": "abc123", "company": "abc123", "phone": "xyz789", "phone2": "abc123", "homePhone": "abc123", "fax": "abc123", "mobilePhone": "abc123", "departm
... (truncated)
```
---
## Security Profiles
### securityProfiles
**Type:** Query
**Returns:** `a SecurityProfilePagingResponse!`
Retrieve SecurityProfile by filter
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iFilter` | No | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
| `orderBy` | `iOrderBy` | No | Default = {property : "id", direction : ASC} |
**Example Query:**
```graphql
query securityProfiles( $filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy ) { securityProfiles( filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy ) { lowerCursor upperCursor hasMoreItems objectList { ...SecurityProfileFragment } } }
```
**Example Variables:**
```json
{ "filter": iFilter, "limit": 123, "cursor": "4", "orderBy": {"property": "id", "direction": "ASC"} }
```
**Example Response:**
```json
{ "data": { "securityProfiles": { "lowerCursor": 4, "upperCursor": "4", "hasMoreItems": true, "objectList": [SecurityProfile] } } }
```
---
### createUserSecurityProfile
**Type:** Mutation
**Returns:** `a UserSecurityProfile`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | `String!` | Yes | — |
| `setup` | `iUserSecurityProfile` | No | — |
**Example Query:**
```graphql
mutation createUserSecurityProfile( $name: String!, $setup: iUserSecurityProfile ) { createUserSecurityProfile( name: $name, setup: $setup ) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } properties { ...SecurityPropertyFragment } securityRoles } }
```
**Example Variables:**
```json
{ "name": "abc123", "setup": iUserSecurityProfile }
```
**Example Response:**
```json
{ "data": { "createUserSecurityProfile": { "id": "4", "name": "xyz789", "description": "xyz789", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "properties": [SecurityProperty], "securityRoles": ["ADMIN"] } } }
```
---
### addProfileToUser
**Type:** Mutation
**Returns:** `a Boolean!`
Add one or many SecurityProfiles to one or many Users
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | — |
| `to` | `[ID!]!` | Yes | — |
**Example Query:**
```graphql
mutation addProfileToUser( $id: [ID!]!, $to: [ID!]! ) { addProfileToUser( id: $id, to: $to ) }
```
**Example Variables:**
```json
{"id": ["4"], "to": [4]}
```
**Example Response:**
```json
{"data": {"addProfileToUser": true}}
```
---
### addRoleToUser
**Type:** Mutation
**Returns:** `a Boolean!`
Add one or many Role(s) to one or many User(s)
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | — |
| `to` | `[ID!]!` | Yes | — |
**Example Query:**
```graphql
mutation addRoleToUser( $id: [ID!]!, $to: [ID!]! ) { addRoleToUser( id: $id, to: $to ) }
```
**Example Variables:**
```json
{"id": ["4"], "to": [4]}
```
**Example Response:**
```json
{"data": {"addRoleToUser": true}}
```
---
## Projects
### projects
**Type:** Query
**Returns:** `a ProjectPagingResponse!`
Retrieve Projects by filter
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iFilter` | No | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
| `orderBy` | `iOrderBy` | No | Default = {property : "id", direction : ASC} |
**Example Query:**
```graphql
query projects( $filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy ) { projects( filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy ) { lowerCursor upperCursor hasMoreItems objectList { ...ProjectFragment } } }
```
**Example Variables:**
```json
{ "filter": iFilter, "limit": 123, "cursor": "4", "orderBy": {"property": "id", "direction": "ASC"} }
```
**Example Response:**
```json
{ "data": { "projects": { "lowerCursor": 4, "upperCursor": 4, "hasMoreItems": true, "objectList": [Project] } } }
```
---
### projectById
**Type:** Query
**Returns:** `[Project!]`
Retrieve Projects by ID or [ID]
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | — |
**Example Query:**
```graphql
query projectById($id: [ID!]!) { projectById(id: $id) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } trashDate trashUser { ...UserFragment } endDate metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } status workflowName approvalStatus { ...ApprovalActivityStatusFragment } approvalSummary approvals { ...ApprovalCycleFragment } assetApprovals { ...ApprovalCycleFragment } children { ...PagingResponseFragment } assetWorkflow { ...WorkflowFragment } processes { ...ProcessFragment } projectTemplate { ...ProjectTemplateFragment } priority colorSpace { ...ColorSpaceFragment } viewingCondition { ...ViewingConditionFragment } reversedView customer { ...CustomerFragment } parents { ...ContainerFragment } mainAsset { ...AssetFragment } siteId productionParticipants { ...ProductionParticipantFragment } notes { ...NoteFragment } trimmedHeight trimmedWidth nbPages accessControls deadlines { ...ProjectDeadlineFragment } } }
```
**Example Variables:**
```json
{"id": [4]}
```
**Example Response:**
```json
{ "data": { "projectById": [ { "id": 4, "name": "abc123", "description": "xyz789", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "trashDate": "2007-12-03T10:15:30Z", "trashUser": User, "endDate": "2007-12-03T10:15:30Z", "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "status": ["ACTIVE"], "workflowName": "abc123", "approvalStatus": [ApprovalActivityStatus], "approvalSummary": "NONE", "approvals": [ApprovalCycle], "assetApprovals": [ApprovalCycle], "children": PagingResponse, "assetWorkflow": Workflow, "processes": [Process], "projectTemplate": ProjectTemplate, "priority": 123, "colorSpace": ColorSpace, "viewingCondition": ViewingCondition, "reversedView": true, "customer": Cus
... (truncated)
```
---
### projectTemplates
**Type:** Query
**Returns:** `a ProjectTemplateResponse!`
Retrieve Project Templates
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iFilter` | No | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
| `orderBy` | `iOrderBy` | No | Default = {property : "id", direction : ASC} |
**Example Query:**
```graphql
query projectTemplates( $filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy ) { projectTemplates( filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy ) { lowerCursor upperCursor hasMoreItems objectList { ...ProjectTemplateFragment } } }
```
**Example Variables:**
```json
{ "filter": iFilter, "limit": 123, "cursor": 4, "orderBy": {"property": "id", "direction": "ASC"} }
```
**Example Response:**
```json
{ "data": { "projectTemplates": { "lowerCursor": 4, "upperCursor": 4, "hasMoreItems": true, "objectList": [ProjectTemplate] } } }
```
---
### createProject
**Type:** Mutation
**Returns:** `a Project!`
security role handle by the mutationFetcher itself To create a Project for a Customer with a name
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `customerId` | `ID!` | Yes | — |
| `name` | `String!` | Yes | — |
| `setup` | `iProjectSetup` | No | — |
| `inputs` | `[iInputFile!]` | Yes | — |
**Example Query:**
```graphql
mutation createProject( $customerId: ID!, $name: String!, $setup: iProjectSetup, $inputs: [iInputFile!] ) { createProject( customerId: $customerId, name: $name, setup: $setup, inputs: $inputs ) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } trashDate trashUser { ...UserFragment } endDate metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } status workflowName approvalStatus { ...ApprovalActivityStatusFragment } approvalSummary approvals { ...ApprovalCycleFragment } assetApprovals { ...ApprovalCycleFragment } children { ...PagingResponseFragment } assetWorkflow { ...WorkflowFragment } processes { ...ProcessFragment } projectTemplate { ...ProjectTemplateFragment } priority colorSpace { ...ColorSpaceFragment } viewingCondition { ...ViewingConditionFragment } reversedView customer { ...CustomerFragment } parents { ...ContainerFragment } mainAsset { ...AssetFragment } siteId productionParticipants { ...ProductionParticipantFragment } notes { ...NoteFragment } trimmedHeight trimmedWidth nbPages accessControls deadlines { ...ProjectDeadlineFragment } } }
```
**Example Variables:**
```json
{ "customerId": "4", "name": "xyz789", "setup": iProjectSetup, "inputs": [iInputFile] }
```
**Example Response:**
```json
{ "data": { "createProject": { "id": "4", "name": "abc123", "description": "abc123", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "trashDate": "2007-12-03T10:15:30Z", "trashUser": User, "endDate": "2007-12-03T10:15:30Z", "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "status": ["ACTIVE"], "workflowName": "xyz789", "approvalStatus": [ApprovalActivityStatus], "approvalSummary": "NONE", "approvals": [ApprovalCycle], "assetApprovals": [ApprovalCycle], "children": PagingResponse, "assetWorkflow": Workflow, "processes": [Process], "projectTemplate": ProjectTemplate, "priority": 987, "colorSpace": ColorSpace, "viewingCondition": ViewingCondition, "reversedView": true, "customer": C
... (truncated)
```
---
### deleteProject
**Type:** Mutation
**Returns:** `a Boolean!`
To delete a Project. By default the project is moved to the trash
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | id of the Project |
| `now` | `Boolean` | No | flag to be able to immediately delete the Project otherwise it is moved to the trash. Default = false |
**Example Query:**
```graphql
mutation deleteProject( $id: [ID!]!, $now: Boolean ) { deleteProject( id: $id, now: $now ) }
```
**Example Variables:**
```json
{"id": [4], "now": false}
```
**Example Response:**
```json
{"data": {"deleteProject": false}}
```
---
### createFolder
**Type:** Mutation
**Returns:** `a Folder!`
To create a Folder
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `name` | `String!` | Yes | — |
| `in` | `ID!` | Yes | — |
| `setup` | `iFolderSetup` | No | — |
**Example Query:**
```graphql
mutation createFolder( $name: String!, $in: ID!, $setup: iFolderSetup ) { createFolder( name: $name, in: $in, setup: $setup ) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } trashDate trashUser { ...UserFragment } metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } readOnly productionSettings { ...ProductionSettingsFragment } processes { ...ProcessFragment } accessControlList { ...AccessControlListFragment } children { ...PagingResponseFragment } project { ...ProjectFragment } parents { ...ContainerFragment } path { ...FolderFragment } color icon } }
```
**Example Variables:**
```json
{ "name": "xyz789", "in": 4, "setup": iFolderSetup }
```
**Example Response:**
```json
{ "data": { "createFolder": { "id": 4, "name": "xyz789", "description": "abc123", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "trashDate": "2007-12-03T10:15:30Z", "trashUser": User, "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "readOnly": true, "productionSettings": ProductionSettings, "processes": [Process], "accessControlList": AccessControlList, "children": PagingResponse, "project": Project, "parents": [Container], "path": [Folder], "color": "xyz789", "icon": 4 } } }
```
---
## Assets
### assets
**Type:** Query
**Returns:** `an AssetPagingResponse!`
Retrieve Assets by filter
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iFilter` | No | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
| `orderBy` | `iOrderBy` | No | Default = {property : "id", direction : ASC} |
**Example Query:**
```graphql
query assets( $filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy ) { assets( filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy ) { lowerCursor upperCursor hasMoreItems objectList { ...AssetFragment } } }
```
**Example Variables:**
```json
{ "filter": iFilter, "limit": 987, "cursor": 4, "orderBy": {"property": "id", "direction": "ASC"} }
```
**Example Response:**
```json
{ "data": { "assets": { "lowerCursor": "4", "upperCursor": "4", "hasMoreItems": false, "objectList": [Asset] } } }
```
---
### assetById
**Type:** Query
**Returns:** `[Asset!]`
Retrieve Assets by ID or [ID]
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | — |
**Example Query:**
```graphql
query assetById($id: [ID!]!) { assetById(id: $id) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } trashDate trashUser { ...UserFragment } metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } status approvalSummary approvalStatus { ...ApprovalActivityStatusFragment } approvals { ...ApprovalCycleFragment } workflowName isAlias processes { ...ProcessFragment } uuid priority isCheckedOut checkedOutBy { ...UserFragment } privateWorkingRevision isArchived archiveId colorSpace { ...ColorSpaceFragment } viewingCondition { ...ViewingConditionFragment } project { ...ProjectFragment } parents { ...ContainerFragment } numberOfRevisions expirationDate medias { ...MediaFragment } notes { ...NoteFragment } relationFrom { ...RelationFragment } relationTo { ...RelationFragment } accessControls } }
```
**Example Variables:**
```json
{"id": [4]}
```
**Example Response:**
```json
{ "data": { "assetById": [ { "id": 4, "name": "xyz789", "description": "abc123", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "trashDate": "2007-12-03T10:15:30Z", "trashUser": User, "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "status": ["ACTIVE"], "approvalSummary": "NONE", "approvalStatus": [ApprovalActivityStatus], "approvals": [ApprovalCycle], "workflowName": "abc123", "isAlias": false, "processes": [Process], "uuid": "abc123", "priority": 987, "isCheckedOut": false, "checkedOutBy": User, "privateWorkingRevision": 987, "isArchived": true, "archiveId": 4, "colorSpace": ColorSpace, "viewingCondition": ViewingCondition, "project": Project, "parents": [Container], "numberO
... (truncated)
```
---
### createAsset
**Type:** Mutation
**Returns:** `an AssetCreationStatus!`
To create an Asset.
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `in` | `[ID!]!` | Yes | — |
| `name` | `String` | No | — |
| `checkedOut` | `Boolean` | No | Default = false |
| `setup` | `iAssetSetup` | No | — |
| `input` | `iInputFile` | No | — |
**Example Query:**
```graphql
mutation createAsset( $in: [ID!]!, $name: String, $checkedOut: Boolean, $setup: iAssetSetup, $input: iInputFile ) { createAsset( in: $in, name: $name, checkedOut: $checkedOut, setup: $setup, input: $input ) { created asset { ...AssetFragment } } }
```
**Example Variables:**
```json
{ "in": ["4"], "name": "xyz789", "checkedOut": false, "setup": iAssetSetup, "input": iInputFile }
```
**Example Response:**
```json
{ "data": { "createAsset": {"created": false, "asset": Asset} } }
```
---
### deleteAsset
**Type:** Mutation
**Returns:** `a Boolean!`
To delete an Asset. By default the Asset is moved to the trash
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | id of the Asset |
| `now` | `Boolean` | No | flag to be able to immediately delete the Asset otherwise it is moved to the trash. Default = false |
**Example Query:**
```graphql
mutation deleteAsset( $id: [ID!]!, $now: Boolean ) { deleteAsset( id: $id, now: $now ) }
```
**Example Variables:**
```json
{"id": [4], "now": false}
```
**Example Response:**
```json
{"data": {"deleteAsset": false}}
```
---
### downloadAsset
**Type:** Query
**Returns:** `a Stream`
Stream an Asset
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `ID!` | Yes | — |
| `revision` | `Int` | No | Default = 0 |
**Example Query:**
```graphql
query downloadAsset( $id: ID!, $revision: Int ) { downloadAsset( id: $id, revision: $revision ) }
```
**Example Variables:**
```json
{"id": "4", "revision": 0}
```
**Example Response:**
```json
{"data": {"downloadAsset": Stream}}
```
---
## Organizations & Customers
### customers
**Type:** Query
**Returns:** `a CustomerPagingResponse!`
Retrieve Customers by filter
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iFilter` | No | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
| `orderBy` | `iOrderBy` | No | Default = {property : "id", direction : ASC} |
**Example Query:**
```graphql
query customers( $filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy ) { customers( filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy ) { lowerCursor upperCursor hasMoreItems objectList { ...CustomerFragment } } }
```
**Example Variables:**
```json
{ "filter": iFilter, "limit": 123, "cursor": 4, "orderBy": {"property": "id", "direction": "ASC"} }
```
**Example Response:**
```json
{ "data": { "customers": { "lowerCursor": "4", "upperCursor": 4, "hasMoreItems": false, "objectList": [Customer] } } }
```
---
### customerById
**Type:** Query
**Returns:** `[Customer!]`
Retrieve Customers by ID or [ID]
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | — |
**Example Query:**
```graphql
query customerById($id: [ID!]!) { customerById(id: $id) { id name description lastModificationDate lastModificationUser { ...UserFragment } creationDate creationUser { ...UserFragment } metadatas { ...MetadataValueFragment } metadataProperties { ...MetadataValueFragment } sites defaultSite defaultProjectTemplate { ...ProjectTemplateFragment } projectTemplates { ...ProjectTemplateFragment } code phone phone2 www fax shippingAddress { ...AddressFragment } billingAddress { ...AddressFragment } organization { ...OrganizationFragment } children { ...PagingResponseFragment } emailTemplates { ...EmailTemplateFragment } notificationTemplates { ...NotificationTemplateFragment } securityConfiguration { ...SecurityConfigurationFragment } notifications { ...NotificationFragment } } }
```
**Example Variables:**
```json
{"id": [4]}
```
**Example Response:**
```json
{ "data": { "customerById": [ { "id": 4, "name": "abc123", "description": "abc123", "lastModificationDate": "2007-12-03T10:15:30Z", "lastModificationUser": User, "creationDate": "2007-12-03T10:15:30Z", "creationUser": User, "metadatas": [MetadataValue], "metadataProperties": [MetadataValue], "sites": ["abc123"], "defaultSite": "xyz789", "defaultProjectTemplate": ProjectTemplate, "projectTemplates": [ProjectTemplate], "code": "xyz789", "phone": "abc123", "phone2": "abc123", "www": "xyz789", "fax": "abc123", "shippingAddress": Address, "billingAddress": Address, "organization": Organization, "children": PagingResponse, "emailTemplates": [EmailTemplate], "notificationTemplates": [NotificationTemplate], "securityConfiguration": SecurityConfiguration, "notifications": [Notification] } ] } }
```
---
### groups
**Type:** Query
**Returns:** `a GroupPagingResponse!`
Retrieve visible groups by the current logged user
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iFilter` | No | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
| `orderBy` | `iOrderBy` | No | Default = {property : "id", direction : ASC} |
**Example Query:**
```graphql
query groups( $filter: iFilter, $limit: Int, $cursor: ID, $orderBy: iOrderBy ) { groups( filter: $filter, limit: $limit, cursor: $cursor, orderBy: $orderBy ) { lowerCursor upperCursor hasMoreItems objectList { ...GroupFragment } } }
```
**Example Variables:**
```json
{ "filter": iFilter, "limit": 123, "cursor": 4, "orderBy": {"property": "id", "direction": "ASC"} }
```
**Example Response:**
```json
{ "data": { "groups": { "lowerCursor": "4", "upperCursor": 4, "hasMoreItems": false, "objectList": [Group] } } }
```
---
### addUserToGroup
**Type:** Mutation
**Returns:** `a Boolean!`
Add one or many Users to one or many Groups that are not System Groups. All the Users provided will be added to each Groups provided
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | — |
| `to` | `[ID!]!` | Yes | — |
**Example Query:**
```graphql
mutation addUserToGroup( $id: [ID!]!, $to: [ID!]! ) { addUserToGroup( id: $id, to: $to ) }
```
**Example Variables:**
```json
{"id": ["4"], "to": [4]}
```
**Example Response:**
```json
{"data": {"addUserToGroup": false}}
```
---
## Search
### search
**Type:** Query
**Returns:** `a SearchResponse`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `filter` | `iSearchFilter!` | Yes | — |
| `facets` | `[String!]` | Yes | — |
| `limit` | `Int` | No | — |
| `cursor` | `ID` | No | — |
**Example Query:**
```graphql
query search( $filter: iSearchFilter!, $facets: [String!], $limit: Int, $cursor: ID ) { search( filter: $filter, facets: $facets, limit: $limit, cursor: $cursor ) { upperCursor hasMoreItems objectList { ...EntityFragment } scores facets { ...FacetFragment } } }
```
**Example Variables:**
```json
{ "filter": iSearchFilter, "facets": ["abc123"], "limit": 987, "cursor": 4 }
```
**Example Response:**
```json
{ "data": { "search": { "upperCursor": "4", "hasMoreItems": false, "objectList": [Entity], "scores": [987.65], "facets": [Facet] } } }
```
---
## Approvals
### approve
**Type:** Mutation
**Returns:** `a Boolean!`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | The id(s) of the request approval(s) to approve |
| `comment` | `String` | No | The comment associated to the approve action |
| `checkViewingCondition` | `Boolean` | No | Specify if the viewing condition should be checked or not. Default = true |
**Example Query:**
```graphql
mutation approve( $id: [ID!]!, $comment: String, $checkViewingCondition: Boolean ) { approve( id: $id, comment: $comment, checkViewingCondition: $checkViewingCondition ) }
```
**Example Variables:**
```json
{ "id": [4], "comment": "xyz789", "checkViewingCondition": true }
```
**Example Response:**
```json
{"data": {"approve": true}}
```
---
### approveObject
**Type:** Mutation
**Returns:** `a Boolean!`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `[ID!]!` | Yes | The id(s) of the object(s) to approve |
| `comment` | `String` | No | The comment associated to the approve action |
| `checkViewingCondition` | `Boolean` | No | Specify if the viewing condition should be checked or not. Default = true |
**Example Query:**
```graphql
mutation approveObject( $id: [ID!]!, $comment: String, $checkViewingCondition: Boolean ) { approveObject( id: $id, comment: $comment, checkViewingCondition: $checkViewingCondition ) }
```
**Example Variables:**
```json
{ "id": [4], "comment": "xyz789", "checkViewingCondition": true }
```
**Example Response:**
```json
{"data": {"approveObject": false}}
```
---

486
generate_docs.py Normal file
View file

@ -0,0 +1,486 @@
#!/usr/bin/env python3
"""Generate Tier 1 index and Tier 2 reference from parsed API data."""
import json
import re
DOCS_DIR = '/Users/daveporter/Desktop/CODING-2024/DALIM-API/docs'
# Domain groupings based on operation name patterns
DOMAIN_PATTERNS = [
('Authentication & System', [
'connectAs', 'disconnectAs', 'whoami', 'serverInformation', 'authentications',
'createAuthentication', 'deleteAuthentication', 'editAuthentication',
'clientApplications', 'createClientApplication', 'deleteClientApplication', 'editClientApplication',
'getCorsSetting', 'setCorsSetting',
'getLogLevel', 'setLogLevel', 'getLogQueries', 'setLogQueries',
'getMaxQueryComplexity', 'setMaxQueryComplexity', 'getMaxQuerySize', 'setMaxQuerySize',
'getMaxTopLevelFieldCount', 'setMaxTopLevelFieldCount', 'queryStatistics',
'servlets', 'openDialogueConnection', 'closeDialogueConnection', 'dialogueConnections',
'get2FAQRCode', 'getSecret2FAKey', 'send2FAQRCodeByEmail', 'enable2FA', 'disable2FA',
'verify2FACode', 'reset2FAKey',
'myWebAuthnRegistrations', 'webAuthnRegistrations',
'deleteMyWebAuthnRegistration', 'deleteWebAuthnRegistration',
'editMyWebAuthnRegistration', 'editWebAuthnRegistration',
'startWebAuthnRegistration', 'finishWebAuthnRegistration',
'userConnectionById', 'userConnectionByClientId', 'userConnections',
'revokeUserConnection', 'authenticationChanged',
'reIndexAll', 'dummy',
]),
('Users & Profiles', [
'users', 'userById', 'createUser', 'changeUser', 'deleteUser',
'editUser', 'editMyUser', 'moveUser',
'changeMyPassword', 'changeMyProfile', 'resetUserPassword',
'userPreference', 'setUserPreference', 'editUserPreference',
'uploadAvatar', 'userChange', 'whenUserChange',
]),
('Security Profiles', [
'securityProfiles', 'securityProfileById', 'securityRoles',
'createAdminSecurityProfile', 'createDefaultSecurityProfile',
'createUserSecurityProfile', 'createSecurityProfileMask',
'deleteSecurityProfile', 'editSecurityProfile', 'duplicateSecurityProfile',
'addProfileToUser', 'removeProfileFromUser',
'putSecurityProperties', 'removeSecurityProperties',
'putSecurityRoles', 'removeSecurityRoles', 'setSecurityRoles',
]),
('Groups & Roles', [
'groups', 'groupById', 'createGroup', 'deleteGroup', 'editGroup',
'addUserToGroup', 'removeUserFromGroup',
'roles', 'roleById', 'createRole', 'deleteRole', 'editRole',
'addRoleToUser', 'removeRoleFromUser',
]),
('Organizations & Customers', [
'organizations', 'organizationById', 'organizationsByFilter',
'createOrganization', 'deleteOrganization', 'renameOrganization', 'editOrganization',
'uploadOrganizationLogo',
'customers', 'customerById', 'createCustomer', 'deleteCustomer',
'renameCustomer', 'editCustomer',
'addCustomerToGroup', 'addCustomerToOrganization',
'removeCustomerFromGroup', 'removeCustomerFromOrganization',
]),
('Projects', [
'projects', 'projectById', 'projectTemplates', 'projectTemplateById',
'projectFolderById',
'createProject', 'deleteProject', 'renameProject', 'duplicateProject',
'editProject', 'moveProject', 'moveProjectToCustomer',
'createProjectFolder', 'deleteProjectFolder',
'renameProjectFolder', 'editProjectFolder', 'moveProjectFolder',
'createProjectTemplate', 'deleteProjectTemplate',
'editProjectTemplate', 'duplicateProjectTemplate',
'createFolderFromProject', 'createProjectFromFolder',
'participantsByRoleFilter',
'projectChange', 'projectFolderChange',
]),
('Folders', [
'folders', 'folderById',
'createFolder', 'deleteFolder', 'renameFolder', 'moveFolder', 'editFolder',
'folderChange',
]),
('Assets', [
'assets', 'assetById', 'downloadAsset', 'downloadAssetWithNotes',
'createAsset', 'deleteAsset', 'renameAsset', 'moveAsset', 'editAsset',
'checkOutAsset', 'checkInAsset', 'cancelCheckOutAsset',
'copyAssetToFolder', 'createAssetAlias', 'removeAssetAlias',
'mergeAsset', 'reprocessAsset',
'editRevision', 'editInk',
'streamAttachment', 'streamFile',
'uploadFile', 'uploadOn', 'uploadOnMilestone',
'assetChange', 'mediaChange',
]),
('Collections', [
'collections', 'collectionById', 'anyCollections',
'createCollection', 'deleteCollection', 'renameCollection', 'editCollection',
'addObjectToCollection', 'removeObjectFromCollection',
'moveCollection', 'moveObjectFromCollectionToCollection',
'smartCollections', 'smartCollectionById',
'createSmartCollection', 'deleteSmartCollection',
'editSmartCollection', 'renameSmartCollection',
'collectionChange', 'smartCollectionChange',
]),
('Search & Filters', [
'search', 'searchBySmartCollection', 'entityByPath', 'dumpText',
'namedSearchFilters', 'namedSearchFilterById', 'namedSearchFilterByName',
'createNamedSearchFilter', 'deleteNamedSearchFilter', 'editNamedSearchFilter',
]),
('Approvals', [
'approvals', 'approvalsByUser',
'approve', 'approveObject', 'reject', 'rejectObject',
'resetApproval', 'resetObjectApproval',
]),
('Workflows & Processes', [
'workflows', 'workflowsById', 'workflowsByName', 'workflowsByEntity',
'workflowEngine',
'createWorkflow', 'deleteWorkflow', 'editWorkflow', 'duplicateWorkflow',
'startWorkflow', 'cancelWorkflow',
'editWorkflowEngineCapacity',
'processes', 'processById', 'processMonitoring',
'cancelProcess', 'deleteProcess', 'changeProcessPriority',
'startProcess', 'startFileProcess', 'startURLProcess', 'submitURLProcess',
'restartProcess', 'restartActivity',
'processChanged',
]),
('Notes & Annotations', [
'getNotes', 'noteReport',
'createNote', 'deleteNote', 'updateNote', 'editNote',
'replyToNote', 'editReplyOfNote', 'deleteReplyOfNote',
'addNoteAttachment', 'removeNoteAttachment',
'annotationStatuses', 'annotationStatusById',
'createAnnotationStatus', 'deleteAnnotationStatus',
'updateAnnotationStatus', 'editAnnotationStatus',
'proofReadingMarks', 'proofReadingMarksById',
'myProofReadingMarks', 'myProofReadingMarksById',
'createGlobalProofReadingMark', 'createPrivateProofReadingMark',
'deleteAnyProofReadingMark', 'deleteMyProofReadingMark',
'editGlobalProofReadingMark', 'editPrivateProofReadingmark',
'noteChange', 'whenNoteChange', 'proofReadingMarkChange',
]),
('Notifications', [
'notifications', 'userNotifications', 'notificationSender',
'notificationTemplates', 'notificationTemplateById',
'createNotificationTemplate', 'deleteNotificationTemplate',
'editNotificationTemplate', 'editNotifications',
'disableNotifications', 'enableNotifications',
'updateNotificationSender', 'stopStartNotificationSender',
'markAsSeen', 'resendMail',
]),
('Sharing', [
'getSharing', 'sharingById', 'sharingByUser', 'sharings', 'mySharing',
'createSharing', 'deleteSharing', 'deleteMySharing',
'updateSharing', 'editSharing', 'editMySharing', 'share',
]),
('Email & Output Channels', [
'emailTemplates', 'emailTemplateById',
'createEmailTemplate', 'deleteEmailTemplate',
'updateEmailTemplate', 'editEmailTemplate',
'outputChannelGroups', 'outputChannelGroupById', 'outputChannelById',
'createOutputChannelGroup', 'deleteOutputChannelGroup', 'editOutputChannelGroup',
'createEmailOutputChannel', 'deleteEmailOutputChannel', 'editEmailOutputChannel',
'createFileOutputChannel', 'deleteFileOutputChannel', 'editFileOutputChannel',
]),
('Input Channels & Rules', [
'inputChannels', 'inputChannelById',
'createInputChannel', 'deleteInputChannel', 'editInputChannel',
'inputRuleSets', 'inputRuleSetById',
'createInputRuleSet', 'deleteInputRuleSet', 'editInputRuleSet',
]),
('Metadata', [
'metadataDefinitionById', 'metadataDefinitionByRef',
'createMetadataDefinition', 'deleteMetadataDefinition',
'editMetadataDefinition', 'renameMetadataDefinition',
'nameSpaceDefinitions', 'nameSpaceDefinitionById',
'nameSpaceDefinitionByPrefix', 'nameSpaceDefinitionByURI',
'createMetadataNameSpace', 'deleteMetadataNameSpace', 'editMetadataNameSpace',
'getProperties', 'getProperty', 'setProperties', 'setContent', 'deleteProperty',
]),
('Thesaurus & Taxonomy', [
'thesaurus', 'thesaurusById', 'thesaurusByLabel', 'thesaurusByURI',
'createThesaurus', 'deleteThesaurus',
'synSetById', 'synSetByLabel', 'synSetByURI',
'createSynSet', 'deleteSynSet', 'editSynSet',
'reactivateSynSet', 'retireSynSet',
]),
('Volumes & Hosts', [
'volumes', 'volumeById', 'checkVolumeConnectivity',
'createVolume', 'createFileSystemVolume', 'deleteVolume',
'editVolume', 'editFileSystemVolume', 'updateFileSystemVolumeDiskId',
'hosts', 'hostById', 'hostFoldersByPath',
'deleteHost', 'editHost',
]),
('Color Management', [
'colorSpaces', 'colorSpaceById',
'createColorSpace', 'deleteColorSpace', 'editColorSpace',
'iccProfiles', 'iccProfileById', 'iccProfileByName',
'deleteIccProfile', 'uploadIccProfile',
'inks', 'inkById', 'inkByName', 'inkCoverage',
'viewingConditions', 'viewingConditionById',
'createViewingCondition', 'deleteViewingCondition', 'editViewingCondition',
'editIccContext', 'densitometer', 'gamutCheck', 'gamutWarning',
]),
('Layouts', [
'layouts', 'layoutById',
'createLayout', 'deleteLayout', 'editLayout',
]),
('User Actions', [
'userActions', 'userActionById', 'userActionIconNames',
'userActionInstancesById', 'userActionInstancesByUser',
'createUserAction', 'deleteUserAction', 'editUserAction',
'clearUserActionInstancesById', 'clearUserActionInstancesByUser',
'executeUserAction', 'startUserAction',
'uploadUserActionIcon',
'userActionChange', 'whenUserActionChange',
]),
('UI & Files', [
'getUIFiles', 'getUIFileContent', 'getUIFilesByFilter',
'getUIProjects', 'getUIProjectFiles', 'getUIProjectsByFilter',
'getUIProjectsByName', 'getUIProjectsByType',
'createUIFile', 'createAndUploadUIFile', 'deleteUIFile',
'editUIFile', 'saveUIFile', 'uploadUIFile',
'createUIFolder', 'deleteUIFolder',
'createUIProject', 'createUIProjectWithType', 'deleteUIProject',
'editUIProject',
'streamUIFileContent', 'getFileContent', 'saveFile',
]),
('Preflighting & Rasterizing', [
'preflightReport', 'streamPreflightPreview', 'rasterize', 'readBarCode',
]),
('Import / Export', [
'importedFiles', 'getImportConflicts', 'getImportInfo',
'importData', 'exportData', 'uploadImportFile',
'getSchemaExtensions', 'createSchemaExtension', 'deleteSchemaExtension',
'editSchemaExtension',
]),
('Events & Logging', [
'getEventLogs',
'whenObjectChange', 'whenStepStatusChange',
]),
('Relations', [
'createRelation', 'deleteRelation', 'deleteRelationFromObject', 'editRelation',
]),
('Reports', [
'reports', 'getReportURL',
]),
('Trash', [
'trashedObjects', 'deleteAllTrashedObjects', 'deleteObject',
'restoreTrashedObject', 'trashObject', 'unTrashObject', 'unTrashAllObjects',
]),
('Milestones', [
'createMilestone',
]),
('Activities', [
'activities', 'activityById', 'activityIconById', 'activityIcons',
'activityPresets', 'activityVariables',
'deleteActivity', 'deleteActivityIcon', 'deleteActivityPreset',
'duplicateActivity', 'createCustomActivity', 'deleteCustomActivity',
'editActivity', 'editActivityPreset', 'editCustomActivity',
'editActivityEngineCapacity', 'editActivityEngineTemplate',
'uploadActivityIcon',
]),
]
# Seed endpoints for Tier 2 detailed docs
SEED_ENDPOINTS = [
# Auth & System
'connectAs', 'disconnectAs', 'whoami', 'serverInformation',
# Users
'users', 'userById', 'createUser', 'changeUser',
# Security
'securityProfiles', 'createUserSecurityProfile', 'addProfileToUser', 'addRoleToUser',
# Projects
'projects', 'projectById', 'projectTemplates', 'createProject', 'deleteProject', 'createFolder',
# Assets
'assets', 'assetById', 'createAsset', 'deleteAsset', 'downloadAsset',
# Org
'customers', 'customerById', 'groups', 'addUserToGroup',
# Search
'search',
# Approvals
'approve', 'approveObject',
]
def generate_tier1_index(operations):
"""Generate the full capability index."""
lines = []
lines.append("# Dalim ES FUSiON API — Full Capability Index")
lines.append("")
lines.append("This file lists **all** GraphQL operations available in the Dalim ES FUSiON API.")
lines.append("For detailed docs (arguments, types, examples) on actively used endpoints, see `dalim-api-reference.md`.")
lines.append("")
lines.append(f"**Total: {len(operations)} operations** — {sum(1 for o in operations if o['type']=='query')} Queries, {sum(1 for o in operations if o['type']=='mutation')} Mutations, {sum(1 for o in operations if o['type']=='subscription')} Subscriptions")
lines.append("")
# Build a lookup
op_lookup = {op['name']: op for op in operations}
assigned = set()
for domain, names in DOMAIN_PATTERNS:
domain_ops = []
for name in names:
if name in op_lookup:
domain_ops.append(op_lookup[name])
assigned.add(name)
if not domain_ops:
continue
lines.append(f"## {domain}")
lines.append("")
# Group by type within domain
for op_type, label in [('query', 'Queries'), ('mutation', 'Mutations'), ('subscription', 'Subscriptions')]:
typed = [op for op in domain_ops if op['type'] == op_type]
if not typed:
continue
lines.append(f"### {label}")
for op in typed:
desc = op['description'] or 'No description'
# Clean up description
desc = desc.replace('\n', ' ').strip()
if len(desc) > 120:
desc = desc[:117] + '...'
ret = op['response_type']
lines.append(f"- `{op['name']}` — {desc} → `{ret}`")
lines.append("")
# Catch any unassigned operations
unassigned = [op for op in operations if op['name'] not in assigned]
if unassigned:
lines.append("## Other Operations")
lines.append("")
for op_type, label in [('query', 'Queries'), ('mutation', 'Mutations'), ('subscription', 'Subscriptions')]:
typed = [op for op in unassigned if op['type'] == op_type]
if not typed:
continue
lines.append(f"### {label}")
for op in typed:
desc = op['description'] or 'No description'
desc = desc.replace('\n', ' ').strip()
if len(desc) > 120:
desc = desc[:117] + '...'
ret = op['response_type']
lines.append(f"- `{op['name']}` — {desc} → `{ret}`")
lines.append("")
return '\n'.join(lines)
def generate_tier2_reference(operations):
"""Generate detailed reference for seed endpoints."""
op_lookup = {op['name']: op for op in operations}
lines = []
lines.append("# Dalim ES FUSiON API — Active Endpoint Reference")
lines.append("")
lines.append("Detailed documentation for the endpoints actively used in this project.")
lines.append("For a full list of all 466 available operations, see `dalim-api-index.md`.")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## Authentication")
lines.append("")
lines.append("The API uses OAuth2 with HMAC SHA256 signing.")
lines.append("")
lines.append("**Token endpoint:** `https://{HOST}/ES/api/oauth/token`")
lines.append("**GraphQL endpoint:** `https://{HOST}/ES/api/graphql`")
lines.append("")
lines.append("**Get a token:**")
lines.append("```python")
lines.append('token_data = {')
lines.append(' "grant_type": "password",')
lines.append(' "client_id": CLIENT_ID,')
lines.append(' "client_secret": CLIENT_SECRET,')
lines.append(' "username": USERNAME,')
lines.append(' "password": PASSWORD')
lines.append('}')
lines.append('response = requests.post(TOKEN_URL, data=token_data)')
lines.append('access_token = response.json()["access_token"]')
lines.append('headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}')
lines.append("```")
lines.append("")
lines.append("**Dependency chain** (must create in this order):")
lines.append("Security Profiles → Users → Projects → Assets")
lines.append("")
lines.append("---")
lines.append("")
# Group seed endpoints by category
categories = [
("System & Auth", ['connectAs', 'disconnectAs', 'whoami', 'serverInformation']),
("Users", ['users', 'userById', 'createUser', 'changeUser']),
("Security Profiles", ['securityProfiles', 'createUserSecurityProfile', 'addProfileToUser', 'addRoleToUser']),
("Projects", ['projects', 'projectById', 'projectTemplates', 'createProject', 'deleteProject', 'createFolder']),
("Assets", ['assets', 'assetById', 'createAsset', 'deleteAsset', 'downloadAsset']),
("Organizations & Customers", ['customers', 'customerById', 'groups', 'addUserToGroup']),
("Search", ['search']),
("Approvals", ['approve', 'approveObject']),
]
for cat_name, cat_endpoints in categories:
lines.append(f"## {cat_name}")
lines.append("")
for ep_name in cat_endpoints:
if ep_name not in op_lookup:
lines.append(f"### {ep_name}")
lines.append(f"*Not found in API — may have a different name*")
lines.append("")
continue
op = op_lookup[ep_name]
lines.append(f"### {op['name']}")
lines.append("")
lines.append(f"**Type:** {op['type'].capitalize()}")
lines.append(f"**Returns:** `{op['response_type']}`")
lines.append("")
if op['description']:
lines.append(f"{op['description']}")
lines.append("")
if op['arguments']:
lines.append("**Arguments:**")
lines.append("")
lines.append("| Name | Type | Required | Description |")
lines.append("|------|------|----------|-------------|")
for arg in op['arguments']:
req = "Yes" if arg['required'] else "No"
desc = arg['description'] or ''
lines.append(f"| `{arg['name']}` | `{arg['type']}` | {req} | {desc} |")
lines.append("")
if op['example_query']:
lines.append("**Example Query:**")
lines.append("```graphql")
lines.append(op['example_query'])
lines.append("```")
lines.append("")
if op['example_variables']:
lines.append("**Example Variables:**")
lines.append("```json")
lines.append(op['example_variables'])
lines.append("```")
lines.append("")
if op['example_response']:
# Truncate very long responses
resp = op['example_response']
if len(resp) > 800:
resp = resp[:800] + '\n... (truncated)'
lines.append("**Example Response:**")
lines.append("```json")
lines.append(resp)
lines.append("```")
lines.append("")
lines.append("---")
lines.append("")
return '\n'.join(lines)
def main():
with open(f'{DOCS_DIR}/api_parsed.json') as f:
data = json.load(f)
operations = data['operations']
print(f"Loaded {len(operations)} operations")
# Generate Tier 1
tier1 = generate_tier1_index(operations)
tier1_path = f'{DOCS_DIR}/dalim-api-index.md'
with open(tier1_path, 'w') as f:
f.write(tier1)
print(f"Tier 1 index: {len(tier1):,} bytes → {tier1_path}")
# Generate Tier 2
tier2 = generate_tier2_reference(operations)
tier2_path = f'{DOCS_DIR}/dalim-api-reference.md'
with open(tier2_path, 'w') as f:
f.write(tier2)
print(f"Tier 2 reference: {len(tier2):,} bytes → {tier2_path}")
if __name__ == '__main__':
main()

1
javascripts/spectaql.min.js vendored Normal file
View file

@ -0,0 +1 @@
function scrollSpy(){var l=5,e=document.querySelector("html"),c=(e&&(e=window.getComputedStyle(e).scrollPaddingTop)&&"string"==typeof e&&"auto"!==e&&e.endsWith("px")&&(l+=parseInt(e.split("px")[0])),"nav-scroll-active"),i=null,d=[];function t(){i=null;var e=document.querySelectorAll("[data-traverse-target]");Array.prototype.forEach.call(e,function(e){d.push({id:e.id,top:e.offsetTop})})}var n=debounce(function(){t(),o()},500),o=debounce(function(){var e,t,n,o,r=(e=>{for(var t=e+l,n=0;n<d.length;n++){var o=d[n+1];if(t>=d[n].top&&(!o||t<o.top))return n}return-1})(document.documentElement.scrollTop||document.body.scrollTop);r!==i&&(r=d[i=r],e=document.querySelector("."+c),t=s(r=r?document.querySelector('#nav a[href="#'+r.id+'"]'):null),n=(o=s(e))!==t,o&&n&&u(o,!1),t&&n&&u(t,!0),r&&(r.classList.add(c),r.scrollIntoViewIfNeeded?r.scrollIntoViewIfNeeded():!r.scrollIntoView||0<=(o=(o=r).getBoundingClientRect()).top&&0<=o.left&&o.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&o.right<=(window.innerWidth||document.documentElement.clientWidth)||r.scrollIntoView({block:"center",inline:"start"})),e)&&e.classList.remove(c)},100);function u(e,t){for(var n=t?"add":"remove";e;)e.classList[n]("nav-scroll-expand"),e=s(e.parentNode)}function s(e){return e&&e.closest?e.closest(".nav-group-section"):null}setTimeout(function(){t(),o(),window.addEventListener("scroll",o),window.addEventListener("resize",n)},300)}function toggleMenu(){var t="drawer-open",e=document.querySelector("#spectaql .sidebar-open-button"),n=document.querySelector("#spectaql #sidebar .close-button"),o=document.querySelector("#spectaql .drawer-overlay");function r(){var e=document.querySelector("#spectaql #page");e.classList.contains(t)?e.classList.remove(t):e.classList.add(t)}e.addEventListener("click",r),n.addEventListener("click",r),o.addEventListener("click",r)}function debounce(e,t){var n=null;return function(){clearTimeout(n),n=setTimeout(function(){e.apply(null)},t)}}window.addEventListener("DOMContentLoaded",e=>{toggleMenu(),scrollSpy()});

135
parse_api.py Normal file
View file

@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Parse FUSION_API_index.html using regex to extract all GraphQL operations."""
import re
import json
def strip_html(text):
"""Remove HTML tags and clean whitespace."""
text = re.sub(r'<[^>]+>', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def extract_code_text(html):
"""Extract text content from code blocks, stripping span tags."""
return strip_html(html)
def main():
print("Reading HTML file...")
with open('/Users/daveporter/Desktop/CODING-2024/DALIM-API/FUSION_API_index.html', 'r', encoding='utf-8') as f:
html = f.read()
print(f"HTML size: {len(html):,} bytes")
# Extract all operation sections
# Each section starts with <section id="query-xxx" or id="mutation-xxx"
section_pattern = r'<section\s+id="(query|mutation|subscription)-([^"]+)"[^>]*class="operation[^"]*"[^>]*>(.*?)</section>'
sections = re.findall(section_pattern, html, re.DOTALL)
print(f"Found {len(sections)} operation sections")
operations = []
for op_type, op_name, content in sections:
op = {
'name': op_name,
'type': op_type,
'description': '',
'response_type': '',
'arguments': [],
'example_query': '',
'example_variables': '',
'example_response': ''
}
# Description
desc_match = re.search(r'class="operation-description[^"]*"[^>]*>.*?<p>(.*?)</p>', content, re.DOTALL)
if desc_match:
op['description'] = strip_html(desc_match.group(1))
# Response type
resp_match = re.search(r'class="operation-response[^"]*"[^>]*>.*?Returns\s+(.*?)</p>', content, re.DOTALL)
if resp_match:
op['response_type'] = strip_html(resp_match.group(1))
# Arguments from table
args_section = re.search(r'class="operation-arguments[^"]*"[^>]*>(.*?)</div>', content, re.DOTALL)
if args_section:
rows = re.findall(r'<tr>\s*<td>(.*?)</td>\s*<td>(.*?)</td>\s*</tr>', args_section.group(1), re.DOTALL)
for name_cell, desc_cell in rows:
arg_name = ''
arg_type = ''
name_match = re.search(r'class="property-name"[^>]*>(.*?)</span>', name_cell, re.DOTALL)
if name_match:
arg_name = strip_html(name_match.group(1))
type_match = re.search(r'class="property-type"[^>]*>(.*?)</span>', name_cell, re.DOTALL)
if type_match:
arg_type = strip_html(type_match.group(1))
required = 'required' in name_cell.lower() or '!' in arg_type
desc = strip_html(desc_cell)
if arg_name:
op['arguments'].append({
'name': arg_name,
'type': arg_type,
'description': desc,
'required': required
})
# Example query
query_example = re.search(r'class="[^"]*operation-query-example[^"]*"[^>]*>.*?<pre><code[^>]*>(.*?)</code></pre>', content, re.DOTALL)
if query_example:
op['example_query'] = strip_html(query_example.group(1))
# Example variables
vars_example = re.search(r'class="[^"]*operation-variables-example[^"]*"[^>]*>.*?<pre><code[^>]*>(.*?)</code></pre>', content, re.DOTALL)
if vars_example:
op['example_variables'] = strip_html(vars_example.group(1))
# Example response
resp_example = re.search(r'class="[^"]*operation-response-example[^"]*"[^>]*>.*?<pre><code[^>]*>(.*?)</code></pre>', content, re.DOTALL)
if resp_example:
op['example_response'] = strip_html(resp_example.group(1))
operations.append(op)
# Count stats
queries = [op for op in operations if op['type'] == 'query']
mutations = [op for op in operations if op['type'] == 'mutation']
subscriptions = [op for op in operations if op['type'] == 'subscription']
print(f"\nResults:")
print(f" Queries: {len(queries)}")
print(f" Mutations: {len(mutations)}")
print(f" Subscriptions: {len(subscriptions)}")
with_desc = sum(1 for op in operations if op['description'])
with_args = sum(1 for op in operations if op['arguments'])
with_resp = sum(1 for op in operations if op['response_type'])
with_example = sum(1 for op in operations if op['example_query'])
print(f" With description: {with_desc}/{len(operations)}")
print(f" With arguments: {with_args}/{len(operations)}")
print(f" With response type: {with_resp}/{len(operations)}")
print(f" With example query: {with_example}/{len(operations)}")
# Save
with open('/Users/daveporter/Desktop/CODING-2024/DALIM-API/docs/api_parsed.json', 'w') as f:
json.dump({'operations': operations}, f, indent=2)
print("\nSaved to docs/api_parsed.json")
# Print a few samples
for name in ['activities', 'createProject', 'createUser', 'search', 'assets']:
matches = [op for op in operations if op['name'] == name]
if matches:
op = matches[0]
print(f"\n--- {op['type']} {op['name']} ---")
print(f" Desc: {op['description'][:100]}")
print(f" Response: {op['response_type']}")
print(f" Args ({len(op['arguments'])}):")
for a in op['arguments'][:3]:
print(f" {a['name']}: {a['type']} {'(required)' if a['required'] else ''} - {a['description'][:60]}")
if len(op['arguments']) > 3:
print(f" ... and {len(op['arguments'])-3} more")
if __name__ == '__main__':
main()

1
stylesheets/spectaql.min.css vendored Normal file

File diff suppressed because one or more lines are too long