mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-18 20:30:28 -06:00
Compare commits
12 Commits
@papra/app
...
@papra/doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1996b51b4d | ||
|
|
734027f00c | ||
|
|
557cde940c | ||
|
|
26a83052bd | ||
|
|
5aac3f7ba6 | ||
|
|
0ddc2340f0 | ||
|
|
438a31171c | ||
|
|
53bf93f128 | ||
|
|
b400b3f18d | ||
|
|
0627ec25a4 | ||
|
|
72e5a9a4de | ||
|
|
268ac8e358 |
20
.github/workflows/release-docker.yaml
vendored
20
.github/workflows/release-docker.yaml
vendored
@@ -1,9 +1,12 @@
|
|||||||
name: Release new versions
|
name: Release new versions
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
inputs:
|
||||||
- '@papra/app-server@*'
|
version:
|
||||||
|
description: 'Version to release'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -14,9 +17,6 @@ jobs:
|
|||||||
name: Release Docker images
|
name: Release Docker images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get release version from tag
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/@papra/app-server@}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
corentinth/papra:latest-root
|
corentinth/papra:latest-root
|
||||||
corentinth/papra:${{ env.RELEASE_VERSION }}-root
|
corentinth/papra:${{ inputs.version }}-root
|
||||||
ghcr.io/papra-hq/papra:latest-root
|
ghcr.io/papra-hq/papra:latest-root
|
||||||
ghcr.io/papra-hq/papra:${{ env.RELEASE_VERSION }}-root
|
ghcr.io/papra-hq/papra:${{ inputs.version }}-root
|
||||||
|
|
||||||
- name: Build and push rootless Docker image
|
- name: Build and push rootless Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
corentinth/papra:latest
|
corentinth/papra:latest
|
||||||
corentinth/papra:latest-rootless
|
corentinth/papra:latest-rootless
|
||||||
corentinth/papra:${{ env.RELEASE_VERSION }}-rootless
|
corentinth/papra:${{ inputs.version }}-rootless
|
||||||
ghcr.io/papra-hq/papra:latest
|
ghcr.io/papra-hq/papra:latest
|
||||||
ghcr.io/papra-hq/papra:latest-rootless
|
ghcr.io/papra-hq/papra:latest-rootless
|
||||||
ghcr.io/papra-hq/papra:${{ env.RELEASE_VERSION }}-rootless
|
ghcr.io/papra-hq/papra:${{ inputs.version }}-rootless
|
||||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -40,4 +40,13 @@ jobs:
|
|||||||
title: "chore(release): update versions"
|
title: "chore(release): update versions"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Trigger Docker build
|
||||||
|
if: steps.changesets.outputs.published == 'true' && contains(fromJson(steps.changesets.outputs.publishedPackages).*.name, '@papra/app-server')
|
||||||
|
run: |
|
||||||
|
VERSION=$(echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[] | select(.name=="@papra/app-server") | .version')
|
||||||
|
echo "VERSION: $VERSION"
|
||||||
|
gh workflow run release-docker.yaml -f version="$VERSION"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ Papra is currently in **beta**. The core functionality is stable and usable, but
|
|||||||
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
||||||
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||||
- **Folder ingestion**: Automatically import documents from a folder.
|
- **Folder ingestion**: Automatically import documents from a folder.
|
||||||
|
- **CLI**: Manage your documents from the command line.
|
||||||
|
- **API, SDK and webhooks**: Build your own applications on top of Papra.
|
||||||
- *In progress:* **i18n**: Support for multiple languages.
|
- *In progress:* **i18n**: Support for multiple languages.
|
||||||
- *Coming soon:* **SDK and API**: Build your own applications on top of Papra.
|
|
||||||
- *Coming soon:* **CLI**: Manage your documents from the command line.
|
|
||||||
- *Coming soon:* **Document sharing**: Share documents with others.
|
- *Coming soon:* **Document sharing**: Share documents with others.
|
||||||
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.
|
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.
|
||||||
- *Coming maybe one day:* **Mobile app**: Access and upload documents on the go.
|
- *Coming maybe one day:* **Mobile app**: Access and upload documents on the go.
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# @papra/docs
|
# @papra/docs
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#295](https://github.com/papra-hq/papra/pull/295) [`438a311`](https://github.com/papra-hq/papra/commit/438a31171c606138c4b7fa299fdd58dcbeaaf298) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added support for custom oauth2 providers
|
||||||
|
|
||||||
|
- [#293](https://github.com/papra-hq/papra/pull/293) [`53bf93f`](https://github.com/papra-hq/papra/commit/53bf93f128b54ad1d3553e18680c87ab23155f8d) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added a papra docker-compose.yml generator
|
||||||
|
|
||||||
## 0.3.1
|
## 0.3.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import starlight from '@astrojs/starlight';
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import starlightLinksValidator from 'starlight-links-validator';
|
import starlightLinksValidator from 'starlight-links-validator';
|
||||||
import starlightThemeRapide from 'starlight-theme-rapide';
|
import starlightThemeRapide from 'starlight-theme-rapide';
|
||||||
|
import UnoCSS from 'unocss/astro';
|
||||||
import { sidebar } from './src/content/navigation';
|
import { sidebar } from './src/content/navigation';
|
||||||
|
|
||||||
import posthogRawScript from './src/scripts/posthog.script.js?raw';
|
import posthogRawScript from './src/scripts/posthog.script.js?raw';
|
||||||
|
|
||||||
const posthogApiKey = env.POSTHOG_API_KEY;
|
const posthogApiKey = env.POSTHOG_API_KEY;
|
||||||
@@ -16,6 +18,7 @@ const posthogScript = posthogRawScript.replace('[POSTHOG-API-KEY]', posthogApiKe
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://docs.papra.app',
|
site: 'https://docs.papra.app',
|
||||||
integrations: [
|
integrations: [
|
||||||
|
UnoCSS(),
|
||||||
starlight({
|
starlight({
|
||||||
plugins: [starlightThemeRapide(), starlightLinksValidator({ exclude: ['http://localhost:1221'] })],
|
plugins: [starlightThemeRapide(), starlightLinksValidator({ exclude: ['http://localhost:1221'] })],
|
||||||
title: 'Papra Docs',
|
title: 'Papra Docs',
|
||||||
@@ -38,7 +41,7 @@ export default defineConfig({
|
|||||||
sidebar,
|
sidebar,
|
||||||
favicon: '/favicon.svg',
|
favicon: '/favicon.svg',
|
||||||
head: [
|
head: [
|
||||||
// Add ICO favicon fallback for Safari.
|
// Add ICO favicon fallback for Safari.
|
||||||
{
|
{
|
||||||
tag: 'link',
|
tag: 'link',
|
||||||
attrs: {
|
attrs: {
|
||||||
|
|||||||
1382
apps/docs/package-lock.json
generated
Normal file
1382
apps/docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@papra/docs",
|
"name": "@papra/docs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.9.0",
|
"packageManager": "pnpm@10.9.0",
|
||||||
"description": "Papra documentation website",
|
"description": "Papra documentation website",
|
||||||
@@ -18,11 +18,16 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/starlight": "^0.34.2",
|
"@astrojs/solid-js": "^5.1.0",
|
||||||
"astro": "^5.7.10",
|
"@astrojs/starlight": "^0.34.3",
|
||||||
|
"astro": "^5.8.0",
|
||||||
"sharp": "^0.32.5",
|
"sharp": "^0.32.5",
|
||||||
|
"shiki": "^3.4.2",
|
||||||
"starlight-links-validator": "^0.16.0",
|
"starlight-links-validator": "^0.16.0",
|
||||||
"starlight-theme-rapide": "^0.5.0",
|
"starlight-theme-rapide": "^0.5.0",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"unocss-preset-animations": "^1.2.1",
|
||||||
|
"yaml": "^2.8.0",
|
||||||
"zod-to-json-schema": "^3.24.5"
|
"zod-to-json-schema": "^3.24.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -35,6 +40,7 @@
|
|||||||
"figue": "^2.2.2",
|
"figue": "^2.2.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^15.0.6",
|
"marked": "^15.0.6",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3",
|
||||||
|
"unocss": "0.65.0-beta.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,38 @@
|
|||||||
:root[data-theme='dark'] {
|
:root[data-theme='dark'] {
|
||||||
|
--background: 240 4% 10%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--card: 240 4% 8%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--popover: 240 4% 8%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--primary: 77 100% 74%;
|
||||||
|
--primary-foreground: 0 0% 9%;
|
||||||
|
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--warning: 31 98% 50%;
|
||||||
|
--warning-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 345 4% 17%;
|
||||||
|
--input: 0 0% 89.8%;
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
|
||||||
--background-color: #0c0d0f!important;
|
--background-color: #0c0d0f!important;
|
||||||
--accent-color: #fff!important;
|
--accent-color: #fff!important;
|
||||||
--foreground-color: #9ea3a2!important;
|
--foreground-color: #9ea3a2!important;
|
||||||
@@ -55,4 +89,8 @@
|
|||||||
|
|
||||||
.site-title img {
|
.site-title img {
|
||||||
width: 1.8rem !important;
|
width: 1.8rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre.shiki {
|
||||||
|
border-radius: 0.5rem!important;
|
||||||
|
}
|
||||||
181
apps/docs/src/content/docs/03-guides/04-setup-custom-oauth2.mdx
Normal file
181
apps/docs/src/content/docs/03-guides/04-setup-custom-oauth2.mdx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
title: Setup Custom OAuth2 Providers
|
||||||
|
description: Step-by-step guide to setup custom OAuth2 providers for authentication in your Papra instance.
|
||||||
|
slug: guides/setup-custom-oauth2-providers
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
import { Steps } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This guide will show you how to configure custom OAuth2 providers for authentication in your Papra instance.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Papra's OAuth2 implementation is based on the [Better Auth Generic OAuth plugin](https://www.better-auth.com/docs/plugins/generic-oauth). For more detailed information about the configuration options and advanced usage, please refer to their documentation.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
In order to follow this guide, you need:
|
||||||
|
- A custom OAuth2 provider
|
||||||
|
- An accessible Papra instance
|
||||||
|
- Basic understanding of OAuth2 flows
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To set up custom OAuth2 providers, you'll need to configure the `AUTH_PROVIDERS_CUSTOMS` environment variable with an array of provider configurations. Here's an example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AUTH_PROVIDERS_CUSTOMS='[
|
||||||
|
{
|
||||||
|
"providerId": "custom-oauth2",
|
||||||
|
"providerName": "Custom OAuth2",
|
||||||
|
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
|
||||||
|
"clientId": "your-client-id",
|
||||||
|
"clientSecret": "your-client-secret",
|
||||||
|
"type": "oidc",
|
||||||
|
"discoveryUrl": "https://your-provider.tld/.well-known/openid-configuration",
|
||||||
|
"scopes": ["openid", "profile", "email"]
|
||||||
|
}
|
||||||
|
]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Each provider configuration supports the following fields:
|
||||||
|
|
||||||
|
- `providerId`: A unique identifier for the OAuth provider
|
||||||
|
- `providerName`: The display name of the provider
|
||||||
|
- `providerIconUrl`: URL of the icon to display (optional) you can use a base64 encoded image or an url to a remote image.
|
||||||
|
- `clientId`: OAuth client ID
|
||||||
|
- `clientSecret`: OAuth client secret
|
||||||
|
- `type`: Type of OAuth flow ("oauth2" or "oidc")
|
||||||
|
- `discoveryUrl`: URL to fetch OAuth 2.0 configuration (recommended for OIDC providers)
|
||||||
|
- `authorizationUrl`: URL for the authorization endpoint (required for OAuth2 if not using discoveryUrl)
|
||||||
|
- `tokenUrl`: URL for the token endpoint (required for OAuth2 if not using discoveryUrl)
|
||||||
|
- `userInfoUrl`: URL for the user info endpoint (required for OAuth2 if not using discoveryUrl)
|
||||||
|
- `scopes`: Array of OAuth scopes to request
|
||||||
|
- `redirectURI`: Custom redirect URI (optional)
|
||||||
|
- `responseType`: OAuth response type (defaults to "code")
|
||||||
|
- `prompt`: Controls the authentication experience ("select_account", "consent", "login", "none")
|
||||||
|
- `pkce`: Whether to use PKCE (Proof Key for Code Exchange)
|
||||||
|
- `accessType`: Access type for the authorization request
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Configure your OAuth2 Provider**
|
||||||
|
|
||||||
|
First, you'll need to register your application with your OAuth2 provider. This typically involves:
|
||||||
|
- Creating a new application in your provider's dashboard
|
||||||
|
- Setting up the redirect URI (usually `https://<your-papra-instance>/api/auth/oauth2/callback/:providerId`)
|
||||||
|
- Obtaining the client ID and client secret
|
||||||
|
- Configuring the required scopes
|
||||||
|
|
||||||
|
2. **Configure Papra**
|
||||||
|
|
||||||
|
Add the `AUTH_PROVIDERS_CUSTOMS` environment variable to your Papra instance. Here are some examples:
|
||||||
|
|
||||||
|
For OIDC providers:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"providerId": "custom-oauth2",
|
||||||
|
"providerName": "Custom OAuth2",
|
||||||
|
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
|
||||||
|
"clientId": "your-client-id",
|
||||||
|
"clientSecret": "your-client-secret",
|
||||||
|
"type": "oidc",
|
||||||
|
"discoveryUrl": "https://your-provider.tld/.well-known/openid-configuration",
|
||||||
|
"scopes": ["openid", "profile", "email"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
For standard OAuth2 providers:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"providerId": "custom-oauth2",
|
||||||
|
"providerName": "Custom OAuth2",
|
||||||
|
"providerIconUrl": "https://api.iconify.design/tabler:login-2.svg",
|
||||||
|
"clientId": "your-client-id",
|
||||||
|
"clientSecret": "your-client-secret",
|
||||||
|
"type": "oauth2",
|
||||||
|
"authorizationUrl": "https://your-provider.tld/oauth2/authorize",
|
||||||
|
"tokenUrl": "https://your-provider.tld/oauth2/token",
|
||||||
|
"userInfoUrl": "https://your-provider.tld/oauth2/userinfo",
|
||||||
|
"scopes": ["profile", "email"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The `discoveryUrl` is recommended for OIDC providers as it automatically configures all the necessary endpoints.
|
||||||
|
For standard OAuth2 providers, you'll need to specify the endpoints manually.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
3. **Test the Configuration**
|
||||||
|
|
||||||
|
- Restart your Papra instance to apply the changes
|
||||||
|
- Go to the login page
|
||||||
|
- You should see your custom OAuth2 providers as login options
|
||||||
|
- Try logging in with a test account
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Providers Not Showing Up
|
||||||
|
|
||||||
|
If your OAuth2 providers are not showing up on the login page:
|
||||||
|
- Check that the JSON configuration in `AUTH_PROVIDERS_CUSTOMS` is valid
|
||||||
|
- Ensure all required fields are provided
|
||||||
|
- Verify that the provider IDs are unique
|
||||||
|
|
||||||
|
### Authentication Fails
|
||||||
|
|
||||||
|
If authentication fails:
|
||||||
|
- Verify that the redirect URI is correctly configured in your OAuth2 provider
|
||||||
|
- Check that the client ID and client secret are correct
|
||||||
|
- Ensure the required scopes are properly configured
|
||||||
|
- Check the Papra logs for any error messages
|
||||||
|
|
||||||
|
### OIDC Discovery Issues
|
||||||
|
|
||||||
|
If you're using OIDC and experiencing issues:
|
||||||
|
- Verify that the `discoveryUrl` is accessible
|
||||||
|
- Check that the provider supports OIDC discovery
|
||||||
|
- Ensure the provider's configuration is properly exposed through the discovery endpoint
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Always use HTTPS for your OAuth2 endpoints and ensure your client secret is kept secure.
|
||||||
|
Consider using PKCE (Proof Key for Code Exchange) for additional security by setting `pkce: true` in your configuration.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Multiple Providers
|
||||||
|
|
||||||
|
You can configure multiple custom OAuth2 providers by adding them to the array:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"providerId": "custom-oauth2-1",
|
||||||
|
"providerName": "Custom OAuth2 Provider 1",
|
||||||
|
"type": "oidc",
|
||||||
|
"discoveryUrl": "https://provider1.tld/.well-known/openid-configuration",
|
||||||
|
"clientId": "client-id-1",
|
||||||
|
"clientSecret": "client-secret-1",
|
||||||
|
"scopes": ["openid", "profile", "email"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"providerId": "custom-oauth2-2",
|
||||||
|
"providerName": "Custom OAuth2 Provider 2",
|
||||||
|
"type": "oidc",
|
||||||
|
"discoveryUrl": "https://provider2.tld/.well-known/openid-configuration",
|
||||||
|
"clientId": "client-id-2",
|
||||||
|
"clientSecret": "client-secret-2",
|
||||||
|
"scopes": ["openid", "profile", "email"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
@@ -53,9 +53,9 @@ In today's digital world, managing countless important documents efficiently and
|
|||||||
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
- **Content extraction**: Automatically extract text from images or scanned documents for search.
|
||||||
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
- **Tagging Rules**: Automatically tag documents based on custom rules.
|
||||||
- **Folder ingestion**: Automatically import documents from a folder.
|
- **Folder ingestion**: Automatically import documents from a folder.
|
||||||
|
- **API, SDK and webhooks**: Build your own applications on top of Papra.
|
||||||
|
- **CLI**: Manage your documents from the command line.
|
||||||
- *In progress:* **i18n**: Support for multiple languages.
|
- *In progress:* **i18n**: Support for multiple languages.
|
||||||
- *Coming soon:* **SDK and API**: Build your own applications on top of Papra.
|
|
||||||
- *Coming soon:* **CLI**: Manage your documents from the command line.
|
|
||||||
- *Coming soon:* **Document sharing**: Share documents with others.
|
- *Coming soon:* **Document sharing**: Share documents with others.
|
||||||
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.
|
- *Coming soon:* **Document requests**: Generate upload links for people to add documents.
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const sidebar: StarlightUserConfig['sidebar'] = [
|
|||||||
items: [
|
items: [
|
||||||
{ label: 'Using Docker', slug: 'self-hosting/using-docker' },
|
{ label: 'Using Docker', slug: 'self-hosting/using-docker' },
|
||||||
{ label: 'Using Docker Compose', slug: 'self-hosting/using-docker-compose' },
|
{ label: 'Using Docker Compose', slug: 'self-hosting/using-docker-compose' },
|
||||||
|
{ label: 'Docker Compose Generator', link: '/docker-compose-generator', badge: { text: 'new', variant: 'note' } },
|
||||||
{ label: 'Configuration', slug: 'self-hosting/configuration' },
|
{ label: 'Configuration', slug: 'self-hosting/configuration' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -30,6 +31,10 @@ export const sidebar: StarlightUserConfig['sidebar'] = [
|
|||||||
label: 'Setup Ingestion Folder',
|
label: 'Setup Ingestion Folder',
|
||||||
slug: 'guides/setup-ingestion-folder',
|
slug: 'guides/setup-ingestion-folder',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Setup Custom OAuth2 Providers',
|
||||||
|
slug: 'guides/setup-custom-oauth2-providers',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
363
apps/docs/src/docker-compose-generator/dc-generator.astro
Normal file
363
apps/docs/src/docker-compose-generator/dc-generator.astro
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
---
|
||||||
|
import { codeToHtml } from 'shiki';
|
||||||
|
|
||||||
|
const images = {
|
||||||
|
GitHub: 'ghcr.io/papra-hq/papra',
|
||||||
|
DockerHub: 'corentinth/papra',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultDockerCompose = `
|
||||||
|
services:
|
||||||
|
papra:
|
||||||
|
image: ghcr.io/papra-hq/papra:latest
|
||||||
|
container_name: papra
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 1221:1221
|
||||||
|
environment:
|
||||||
|
- AUTH_SECRET=change-me
|
||||||
|
volumes:
|
||||||
|
- ./app-data:/app/app-data
|
||||||
|
user: 1000:1000
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const dcHtml = await codeToHtml(defaultDockerCompose, { theme: 'vitesse-black', lang: 'yaml' });
|
||||||
|
---
|
||||||
|
|
||||||
|
<p>This tool will help you generate a custom docker-compose.yml file for Papra, tailored to your needs. You can personalize the service name, the port, the auth secret, and the source image.</p>
|
||||||
|
|
||||||
|
<h2 class="mt-8 mb-2">General settings</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="port" class="min-w-32">External port</label>
|
||||||
|
<input id="port" class="input-field" value="1221" type="number" min="1024" max="65535" placeholder="eg: 1221" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="source" class="min-w-32">Image source</label>
|
||||||
|
<select class="input-field mt-0" id="source">
|
||||||
|
{Object.entries(images).map(([registry, imageName]) => <option class="bg-background" value={imageName}>{`${registry} - ${imageName}`}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="service-name" class="min-w-32">Service Name</label>
|
||||||
|
<input id="service-name" class="input-field" value="papra" type="text" placeholder="eg: papra" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label
|
||||||
|
for="auth-secret"
|
||||||
|
class="min-w-32"
|
||||||
|
>
|
||||||
|
Auth secret
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-2 mt-0 w-full">
|
||||||
|
<input class="input-field font-mono" id="auth-secret" type="text" placeholder="eg: 1234567890" />
|
||||||
|
<button class="btn bg-muted" id="refresh-secret"> Refresh </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="volume-path" class="min-w-32">Volume path</label>
|
||||||
|
<input id="volume-path" class="input-field" value="./app-data" type="text" placeholder="eg: ./app-data" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="privileged-mode" class="min-w-32">Privileged mode</label>
|
||||||
|
<div class="flex items-center gap-2 mt-0 w-full">
|
||||||
|
<select class="input-field mt-0" id="privileged-mode">
|
||||||
|
<option value="false" class="bg-background">Rootless</option>
|
||||||
|
<option value="true" class="bg-background">Root</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mt-8 mb-2">Ingestion folder</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="ingestion-enabled" class="min-w-32">Enable ingestion</label>
|
||||||
|
<div class="flex items-center gap-2 mt-0 w-full">
|
||||||
|
<select class="input-field mt-0" id="ingestion-enabled">
|
||||||
|
<option value="false" class="bg-background">Disabled</option>
|
||||||
|
<option value="true" class="bg-background">Enabled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1" id="ingestion-path-container" style="display: none;">
|
||||||
|
<label for="ingestion-path" class="min-w-32">Ingestion path</label>
|
||||||
|
<input id="ingestion-path" class="input-field" value="./ingestion" type="text" placeholder="eg: ./ingestion" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mt-8 mb-2">Intake emails</h2>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="intake-email-enabled" class="min-w-32">Enabled</label>
|
||||||
|
<div class="flex items-center gap-2 mt-0 w-full">
|
||||||
|
<select class="input-field mt-0" id="intake-email-enabled">
|
||||||
|
<option value="false" class="bg-background">Disabled</option>
|
||||||
|
<option value="true" class="bg-background">Enabled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1" id="intake-email-driver-container" style="display: none;">
|
||||||
|
<label for="intake-email-driver" class="min-w-32">Driver</label>
|
||||||
|
<div class="flex items-center gap-2 mt-0 w-full">
|
||||||
|
<select class="input-field mt-0" id="intake-email-driver">
|
||||||
|
<option value="owlrelay" class="bg-background">OwlRelay</option>
|
||||||
|
<option value="random-username" class="bg-background">Cloudflare Email Worker</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="intake-email-owlrelay-config" style="display: none;" class="mt-1">
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="intake-email-owlrelay-api-key" class="min-w-32">API Key</label>
|
||||||
|
<input id="intake-email-owlrelay-api-key" class="input-field" type="text" placeholder="owrl_*****" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="intake-email-owlrelay-webhook-url" class="min-w-32">Webhook URL</label>
|
||||||
|
<input id="intake-email-owlrelay-webhook-url" class="input-field" type="text" placeholder="https://your-instance.com/api/intake-emails/ingest" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="intake-email-cf-worker-config" style="display: none;" class="mt-1">
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<label for="intake-email-cf-email-domain" class="min-w-32">Email domain</label>
|
||||||
|
<input id="intake-email-cf-email-domain" class="input-field" type="text" placeholder="papra.email" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1" id="intake-email-webhook-secret-container" style="display: none;">
|
||||||
|
<label for="intake-email-webhook-secret" class="min-w-32">Webhook secret</label>
|
||||||
|
<div class="flex items-center gap-2 mt-0 w-full">
|
||||||
|
<input class="input-field font-mono" id="intake-email-webhook-secret" type="text" placeholder="a-random-key" />
|
||||||
|
<button class="btn bg-muted" id="refresh-webhook-secret">Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="docker-compose-output" class="mt-12" set:html={dcHtml} />
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-4">
|
||||||
|
<button class="btn bg-muted mt-0" id="download-button">Download docker-compose.yml</button>
|
||||||
|
<button class="btn bg-muted mt-0" id="copy-button">Copy to clipboard</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { codeToHtml } from 'shiki';
|
||||||
|
import { stringify } from 'yaml';
|
||||||
|
|
||||||
|
const portInput = document.getElementById('port') as HTMLInputElement;
|
||||||
|
const sourceSelect = document.getElementById('source') as HTMLSelectElement;
|
||||||
|
const serviceNameInput = document.getElementById('service-name') as HTMLInputElement;
|
||||||
|
const authSecretInput = document.getElementById('auth-secret') as HTMLInputElement;
|
||||||
|
const refreshSecretButton = document.getElementById('refresh-secret');
|
||||||
|
const copyButton = document.getElementById('copy-button');
|
||||||
|
const dockerComposeOutput = document.getElementById('docker-compose-output');
|
||||||
|
const downloadButton = document.getElementById('download-button');
|
||||||
|
const volumePathInput = document.getElementById('volume-path') as HTMLInputElement;
|
||||||
|
const privilegedModeSelect = document.getElementById('privileged-mode') as HTMLSelectElement;
|
||||||
|
const ingestionEnabledSelect = document.getElementById('ingestion-enabled') as HTMLSelectElement;
|
||||||
|
const ingestionPathInput = document.getElementById('ingestion-path') as HTMLInputElement;
|
||||||
|
const ingestionPathContainer = document.getElementById('ingestion-path-container') as HTMLDivElement;
|
||||||
|
const intakeEmailEnabledSelect = document.getElementById('intake-email-enabled') as HTMLSelectElement;
|
||||||
|
const intakeDriverSelect = document.getElementById('intake-email-driver') as HTMLSelectElement;
|
||||||
|
const owlrelayConfig = document.getElementById('intake-email-owlrelay-config') as HTMLDivElement;
|
||||||
|
const cfWorkerConfig = document.getElementById('intake-email-cf-worker-config') as HTMLDivElement;
|
||||||
|
const owlrelayApiKeyInput = document.getElementById('intake-email-owlrelay-api-key') as HTMLInputElement;
|
||||||
|
const owlrelayWebhookUrlInput = document.getElementById('intake-email-owlrelay-webhook-url') as HTMLInputElement;
|
||||||
|
const cfEmailDomainInput = document.getElementById('intake-email-cf-email-domain') as HTMLInputElement;
|
||||||
|
const webhookSecretInput = document.getElementById('intake-email-webhook-secret') as HTMLInputElement;
|
||||||
|
const refreshWebhookSecretButton = document.getElementById('refresh-webhook-secret');
|
||||||
|
|
||||||
|
function getRandomString() {
|
||||||
|
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
return Array.from({ length: 48 }, () => alphabet[Math.floor(Math.random() * alphabet.length)]).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(text: string) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDockerComposeYml() {
|
||||||
|
const serviceName = serviceNameInput.value;
|
||||||
|
const isRootless = privilegedModeSelect.value === 'false';
|
||||||
|
const image = sourceSelect.value;
|
||||||
|
const port = portInput.value;
|
||||||
|
const authSecret = authSecretInput.value;
|
||||||
|
const volumePath = volumePathInput.value;
|
||||||
|
const isIngestionEnabled = ingestionEnabledSelect.value === 'true';
|
||||||
|
const ingestionPath = ingestionPathInput.value;
|
||||||
|
const intakeEmailEnabled = intakeEmailEnabledSelect.value === 'true';
|
||||||
|
const intakeDriver = intakeDriverSelect.value;
|
||||||
|
const webhookSecret = webhookSecretInput.value;
|
||||||
|
|
||||||
|
const version = isRootless ? 'latest' : 'latest-root';
|
||||||
|
const fullImage = `${image}:${version}`;
|
||||||
|
|
||||||
|
const environment = [
|
||||||
|
`AUTH_SECRET=${authSecret}`,
|
||||||
|
isIngestionEnabled && 'INGESTION_FOLDER_IS_ENABLED=true',
|
||||||
|
intakeEmailEnabled && 'INTAKE_EMAILS_IS_ENABLED=true',
|
||||||
|
intakeEmailEnabled && `INTAKE_EMAILS_DRIVER=${intakeDriver}`,
|
||||||
|
intakeEmailEnabled && `INTAKE_EMAILS_WEBHOOK_SECRET=${webhookSecret}`,
|
||||||
|
intakeEmailEnabled && intakeDriver === 'owlrelay' && owlrelayApiKeyInput.value && `OWLRELAY_API_KEY=${owlrelayApiKeyInput.value}`,
|
||||||
|
intakeEmailEnabled && intakeDriver === 'owlrelay' && owlrelayWebhookUrlInput.value && `OWLRELAY_WEBHOOK_URL=${owlrelayWebhookUrlInput.value}`,
|
||||||
|
intakeEmailEnabled && intakeDriver === 'random-username' && cfEmailDomainInput.value && `INTAKE_EMAILS_EMAIL_GENERATION_DOMAIN=${cfEmailDomainInput.value}`,
|
||||||
|
].flat().filter(Boolean);
|
||||||
|
|
||||||
|
const volumes = [
|
||||||
|
`${volumePath}:/app/app-data`,
|
||||||
|
isIngestionEnabled && `${ingestionPath}:/app/ingestion`,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const dc = {
|
||||||
|
services: {
|
||||||
|
[serviceName]: {
|
||||||
|
image: fullImage,
|
||||||
|
container_name: serviceName,
|
||||||
|
restart: 'unless-stopped',
|
||||||
|
ports: [`${port}:1221`],
|
||||||
|
environment,
|
||||||
|
volumes,
|
||||||
|
...(isRootless && {
|
||||||
|
user: '1000:1000',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return stringify(dc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDockerCompose() {
|
||||||
|
const dockerCompose = getDockerComposeYml();
|
||||||
|
|
||||||
|
const html = await codeToHtml(dockerCompose, { theme: 'vitesse-black', lang: 'yaml' });
|
||||||
|
|
||||||
|
if (dockerComposeOutput) {
|
||||||
|
dockerComposeOutput.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
const dockerCompose = getDockerComposeYml();
|
||||||
|
|
||||||
|
copyToClipboard(dockerCompose);
|
||||||
|
|
||||||
|
if (copyButton) {
|
||||||
|
copyButton.textContent = 'Copied!';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (copyButton) {
|
||||||
|
copyButton.textContent = 'Copy to clipboard';
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefreshSecret() {
|
||||||
|
authSecretInput.value = getRandomString();
|
||||||
|
updateDockerCompose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownload() {
|
||||||
|
const dockerCompose = getDockerComposeYml();
|
||||||
|
|
||||||
|
const blob = new Blob([dockerCompose], { type: 'text/yaml' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'docker-compose.yml';
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIngestionEnabledChange() {
|
||||||
|
const isEnabled = ingestionEnabledSelect.value === 'true';
|
||||||
|
ingestionPathContainer.style.display = isEnabled ? 'flex' : 'none';
|
||||||
|
updateDockerCompose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIntakeEmailEnabledChange() {
|
||||||
|
const isEnabled = intakeEmailEnabledSelect.value === 'true';
|
||||||
|
const driverContainer = document.getElementById('intake-email-driver-container');
|
||||||
|
const webhookSecretContainer = document.getElementById('intake-email-webhook-secret-container');
|
||||||
|
|
||||||
|
if (driverContainer) {
|
||||||
|
driverContainer.style.display = isEnabled ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (webhookSecretContainer) {
|
||||||
|
webhookSecretContainer.style.display = isEnabled ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
// Reset driver-specific configs when disabled
|
||||||
|
if (owlrelayConfig) {
|
||||||
|
owlrelayConfig.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (cfWorkerConfig) {
|
||||||
|
cfWorkerConfig.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show the appropriate driver config
|
||||||
|
handleIntakeDriverChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDockerCompose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIntakeDriverChange() {
|
||||||
|
const driver = intakeDriverSelect.value;
|
||||||
|
const isEnabled = intakeEmailEnabledSelect.value === 'true';
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owlrelayConfig) {
|
||||||
|
owlrelayConfig.style.display = driver === 'owlrelay' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
if (cfWorkerConfig) {
|
||||||
|
cfWorkerConfig.style.display = driver === 'random-username' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDockerCompose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefreshWebhookSecret() {
|
||||||
|
webhookSecretInput.value = getRandomString();
|
||||||
|
updateDockerCompose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
portInput.addEventListener('input', updateDockerCompose);
|
||||||
|
sourceSelect.addEventListener('change', updateDockerCompose);
|
||||||
|
serviceNameInput.addEventListener('input', updateDockerCompose);
|
||||||
|
authSecretInput.addEventListener('input', updateDockerCompose);
|
||||||
|
refreshSecretButton?.addEventListener('click', handleRefreshSecret);
|
||||||
|
copyButton?.addEventListener('click', handleCopy);
|
||||||
|
downloadButton?.addEventListener('click', handleDownload);
|
||||||
|
volumePathInput.addEventListener('input', updateDockerCompose);
|
||||||
|
privilegedModeSelect.addEventListener('change', updateDockerCompose);
|
||||||
|
ingestionEnabledSelect.addEventListener('change', handleIngestionEnabledChange);
|
||||||
|
ingestionPathInput.addEventListener('input', updateDockerCompose);
|
||||||
|
intakeEmailEnabledSelect.addEventListener('change', handleIntakeEmailEnabledChange);
|
||||||
|
intakeDriverSelect.addEventListener('change', handleIntakeDriverChange);
|
||||||
|
owlrelayApiKeyInput.addEventListener('input', updateDockerCompose);
|
||||||
|
owlrelayWebhookUrlInput.addEventListener('input', updateDockerCompose);
|
||||||
|
cfEmailDomainInput.addEventListener('input', updateDockerCompose);
|
||||||
|
webhookSecretInput.addEventListener('input', updateDockerCompose);
|
||||||
|
refreshWebhookSecretButton?.addEventListener('click', handleRefreshWebhookSecret);
|
||||||
|
|
||||||
|
authSecretInput.value = getRandomString();
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
updateDockerCompose();
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
handleIngestionEnabledChange();
|
||||||
|
handleIntakeEmailEnabledChange();
|
||||||
|
webhookSecretInput.value = getRandomString();
|
||||||
|
</script>
|
||||||
15
apps/docs/src/pages/docker-compose-generator.astro
Normal file
15
apps/docs/src/pages/docker-compose-generator.astro
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||||
|
import DockerComposeGeneratorComp from '../docker-compose-generator/dc-generator.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<StarlightPage
|
||||||
|
frontmatter={{
|
||||||
|
title: 'Papra docker-compose.yml generator',
|
||||||
|
description: 'Generate a custom docker-compose.yml file for Papra, tailored to your needs.',
|
||||||
|
tableOfContents: false,
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DockerComposeGeneratorComp />
|
||||||
|
</StarlightPage>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"include": [".astro/types.d.ts", "**/*"],
|
"include": [
|
||||||
"exclude": ["dist"]
|
".astro/types.d.ts",
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
97
apps/docs/uno.config.ts
Normal file
97
apps/docs/uno.config.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
presetUno,
|
||||||
|
transformerDirectives,
|
||||||
|
transformerVariantGroup,
|
||||||
|
} from 'unocss';
|
||||||
|
import presetAnimations from 'unocss-preset-animations';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
presets: [
|
||||||
|
presetUno({
|
||||||
|
dark: {
|
||||||
|
dark: '[data-kb-theme="dark"]',
|
||||||
|
light: '[data-kb-theme="light"]',
|
||||||
|
},
|
||||||
|
prefix: '',
|
||||||
|
}),
|
||||||
|
presetAnimations(),
|
||||||
|
],
|
||||||
|
transformers: [transformerVariantGroup(), transformerDirectives()],
|
||||||
|
theme: {
|
||||||
|
colors: {
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
keyframes: {
|
||||||
|
'accordion-down':
|
||||||
|
'{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }',
|
||||||
|
'accordion-up':
|
||||||
|
'{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }',
|
||||||
|
'collapsible-down':
|
||||||
|
'{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }',
|
||||||
|
'collapsible-up':
|
||||||
|
'{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }',
|
||||||
|
'caret-blink': '{ 0%,70%,100% { opacity: 1 } 20%,50% { opacity: 0 } }',
|
||||||
|
},
|
||||||
|
timingFns: {
|
||||||
|
'accordion-down': 'ease-out',
|
||||||
|
'accordion-up': 'ease-out',
|
||||||
|
'collapsible-down': 'ease-out',
|
||||||
|
'collapsible-up': 'ease-out',
|
||||||
|
'caret-blink': 'ease-out',
|
||||||
|
},
|
||||||
|
durations: {
|
||||||
|
'accordion-down': '0.2s',
|
||||||
|
'accordion-up': '0.2s',
|
||||||
|
'collapsible-down': '0.2s',
|
||||||
|
'collapsible-up': '0.2s',
|
||||||
|
'caret-blink': '1.25s',
|
||||||
|
},
|
||||||
|
counts: {
|
||||||
|
'caret-blink': 'infinite',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shortcuts: {
|
||||||
|
'input-field': 'flex h-9 w-full bg-none outline-none rounded-lg border border-border border-solid bg-inherit px-3 py-1 text-sm shadow-none placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow',
|
||||||
|
'btn': 'text-sm font-medium hover:opacity-80 rounded-lg transition-all px-4 py-2 bg-none outline-none border-none cursor-pointer',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
# @papra/app-client
|
# @papra/app-client
|
||||||
|
|
||||||
|
## 0.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#295](https://github.com/papra-hq/papra/pull/295) [`438a311`](https://github.com/papra-hq/papra/commit/438a31171c606138c4b7fa299fdd58dcbeaaf298) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added support for custom oauth2 providers
|
||||||
|
|
||||||
|
- [#291](https://github.com/papra-hq/papra/pull/291) [`0627ec2`](https://github.com/papra-hq/papra/commit/0627ec25a422b7b820b08740cfc2905f9c55c00e) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added invitation system to add users to an organization
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#296](https://github.com/papra-hq/papra/pull/296) [`0ddc234`](https://github.com/papra-hq/papra/commit/0ddc2340f092cf6fe5bf2175b55fb46db7681c36) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Fix register page description
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@papra/app-client",
|
"name": "@papra/app-client",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.9.0",
|
"packageManager": "pnpm@10.9.0",
|
||||||
"description": "Papra frontend client",
|
"description": "Papra frontend client",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ auth.login.form.forgot-password.label: Forgot password?
|
|||||||
auth.login.form.submit: Login
|
auth.login.form.submit: Login
|
||||||
|
|
||||||
auth.register.title: Register to Papra
|
auth.register.title: Register to Papra
|
||||||
auth.register.description: Enter your email or use social login to access your Papra account.
|
auth.register.description: Create an account to start using Papra.
|
||||||
auth.register.register-with-email: Register with email
|
auth.register.register-with-email: Register with email
|
||||||
auth.register.register-with-provider: Register with {{ provider }}
|
auth.register.register-with-provider: Register with {{ provider }}
|
||||||
auth.register.providers.google: Google
|
auth.register.providers.google: Google
|
||||||
@@ -85,7 +85,8 @@ layout.menu.account: Account
|
|||||||
layout.menu.general-settings: General settings
|
layout.menu.general-settings: General settings
|
||||||
layout.menu.intake-emails: Intake emails
|
layout.menu.intake-emails: Intake emails
|
||||||
layout.menu.webhooks: Webhooks
|
layout.menu.webhooks: Webhooks
|
||||||
|
layout.menu.members: Members
|
||||||
|
layout.menu.invitations: Invitations
|
||||||
tagging-rules.field.name: document name
|
tagging-rules.field.name: document name
|
||||||
tagging-rules.field.content: document content
|
tagging-rules.field.content: document content
|
||||||
tagging-rules.operator.equals: equals
|
tagging-rules.operator.equals: equals
|
||||||
@@ -165,6 +166,10 @@ api-errors.document.file_too_big: The document file is too big
|
|||||||
api-errors.intake_email.limit_reached: The maximum number of intake emails for this organization has been reached. Please upgrade your plan to create more intake emails.
|
api-errors.intake_email.limit_reached: The maximum number of intake emails for this organization has been reached. Please upgrade your plan to create more intake emails.
|
||||||
api-errors.user.max_organization_count_reached: You have reached the maximum number of organizations you can create, if you need to create more, please contact support.
|
api-errors.user.max_organization_count_reached: You have reached the maximum number of organizations you can create, if you need to create more, please contact support.
|
||||||
api-errors.default: An error occurred while processing your request.
|
api-errors.default: An error occurred while processing your request.
|
||||||
|
api-errors.organization.invitation_already_exists: An invitation for this email already exists in this organization.
|
||||||
|
api-errors.user.already_in_organization: This user is already in this organization.
|
||||||
|
api-errors.user.organization_invitation_limit_reached: The maximum number of invitations has been reached for today. Please try again tomorrow.
|
||||||
|
api-errors.demo.not_available: This feature is not available in demo
|
||||||
|
|
||||||
api-keys.permissions.documents.title: Documents
|
api-keys.permissions.documents.title: Documents
|
||||||
api-keys.permissions.documents.documents:create: Create documents
|
api-keys.permissions.documents.documents:create: Create documents
|
||||||
@@ -243,3 +248,44 @@ webhooks.delete.confirm.cancel-button: Cancel
|
|||||||
|
|
||||||
webhooks.events.documents.document:created.description: Document created
|
webhooks.events.documents.document:created.description: Document created
|
||||||
webhooks.events.documents.document:deleted.description: Document deleted
|
webhooks.events.documents.document:deleted.description: Document deleted
|
||||||
|
|
||||||
|
organizations.members.title: Members
|
||||||
|
organizations.members.description: Manage your organization members
|
||||||
|
organizations.members.invite-member: Invite member
|
||||||
|
organizations.members.invite-member-disabled-tooltip: Only admins or owners can invite members to the organization
|
||||||
|
organizations.members.remove-from-organization: Remove from organization
|
||||||
|
organizations.members.role: Role
|
||||||
|
organizations.members.roles.owner: Owner
|
||||||
|
organizations.members.roles.admin: Admin
|
||||||
|
organizations.members.roles.member: Member
|
||||||
|
organizations.members.delete.confirm.title: Remove member
|
||||||
|
organizations.members.delete.confirm.message: Are you sure you want to remove this member from the organization?
|
||||||
|
organizations.members.delete.confirm.confirm-button: Remove
|
||||||
|
organizations.members.delete.confirm.cancel-button: Cancel
|
||||||
|
organizations.members.delete.success: Member removed from organization
|
||||||
|
organizations.members.update-role.success: Member role updated
|
||||||
|
|
||||||
|
organizations.invite-member.title: Invite member
|
||||||
|
organizations.invite-member.description: Invite a member to your organization
|
||||||
|
organizations.invite-member.form.email.label: Email
|
||||||
|
organizations.invite-member.form.email.placeholder: 'Example: ada@papra.app'
|
||||||
|
organizations.invite-member.form.email.required: Please enter a valid email address
|
||||||
|
organizations.invite-member.form.role.label: Role
|
||||||
|
organizations.invite-member.form.submit: Invite to organization
|
||||||
|
organizations.invite-member.success.message: Member invited
|
||||||
|
organizations.invite-member.success.description: The email has been invited to the organization.
|
||||||
|
organizations.invite-member.error.message: Failed to invite member
|
||||||
|
|
||||||
|
invitations.list.title: Invitations
|
||||||
|
invitations.list.description: Manage your organization invitations
|
||||||
|
invitations.list.empty.title: No pending invitations
|
||||||
|
invitations.list.empty.description: You haven't been invited to any organizations yet.
|
||||||
|
invitations.list.headers.organization: Organization
|
||||||
|
invitations.list.headers.created: Created
|
||||||
|
invitations.list.headers.actions: Actions
|
||||||
|
invitations.list.actions.accept: Accept
|
||||||
|
invitations.list.actions.reject: Reject
|
||||||
|
invitations.list.actions.accept.success.message: Invitation accepted
|
||||||
|
invitations.list.actions.accept.success.description: The invitation has been accepted.
|
||||||
|
invitations.list.actions.reject.success.message: Invitation rejected
|
||||||
|
invitations.list.actions.reject.success.description: The invitation has been rejected.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ auth.login.form.forgot-password.label: Mot de passe oublié ?
|
|||||||
auth.login.form.submit: Connexion
|
auth.login.form.submit: Connexion
|
||||||
|
|
||||||
auth.register.title: S'inscrire à Papra
|
auth.register.title: S'inscrire à Papra
|
||||||
auth.register.description: Entrez votre email ou utilisez une connexion sociale pour accéder à votre compte Papra.
|
auth.register.description: Créez un compte pour commencer à utiliser Papra.
|
||||||
auth.register.register-with-email: S'inscrire avec email
|
auth.register.register-with-email: S'inscrire avec email
|
||||||
auth.register.register-with-provider: S'inscrire avec {{ provider }}
|
auth.register.register-with-provider: S'inscrire avec {{ provider }}
|
||||||
auth.register.providers.google: Google
|
auth.register.providers.google: Google
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { LocaleKeys } from '@/modules/i18n/locales.types';
|
|
||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import type { LocaleKeys } from '@/modules/i18n/locales.types';
|
||||||
|
import { createSignal, For } from 'solid-js';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
||||||
import { createSignal, For } from 'solid-js';
|
|
||||||
import { API_KEY_PERMISSIONS } from '../api-keys.constants';
|
import { API_KEY_PERMISSIONS } from '../api-keys.constants';
|
||||||
|
|
||||||
export const ApiKeyPermissionsPicker: Component<{ permissions: string[]; onChange: (permissions: string[]) => void }> = (props) => {
|
export const ApiKeyPermissionsPicker: Component<{ permissions: string[]; onChange: (permissions: string[]) => void }> = (props) => {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { ApiKey } from '../api-keys.types';
|
import type { ApiKey } from '../api-keys.types';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
|
import { createMutation, createQuery } from '@tanstack/solid-query';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { For, Match, Show, Suspense, Switch } from 'solid-js';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||||
import { queryClient } from '@/modules/shared/query/query-client';
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { EmptyState } from '@/modules/ui/components/empty';
|
import { EmptyState } from '@/modules/ui/components/empty';
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { A } from '@solidjs/router';
|
|
||||||
import { createMutation, createQuery } from '@tanstack/solid-query';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { For, Match, Show, Suspense, Switch } from 'solid-js';
|
|
||||||
import { deleteApiKey, fetchApiKeys } from '../api-keys.services';
|
import { deleteApiKey, fetchApiKeys } from '../api-keys.services';
|
||||||
|
|
||||||
export const ApiKeyCard: Component<{ apiKey: ApiKey }> = ({ apiKey }) => {
|
export const ApiKeyCard: Component<{ apiKey: ApiKey }> = ({ apiKey }) => {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { setValue } from '@modular-forms/solid';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
|
import { createSignal, Show } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
import { queryClient } from '@/modules/shared/query/query-client';
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
@@ -6,10 +10,6 @@ import { CopyButton } from '@/modules/shared/utils/copy';
|
|||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { setValue } from '@modular-forms/solid';
|
|
||||||
import { A } from '@solidjs/router';
|
|
||||||
import { createSignal, Show } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { API_KEY_PERMISSIONS_LIST } from '../api-keys.constants';
|
import { API_KEY_PERMISSIONS_LIST } from '../api-keys.constants';
|
||||||
import { createApiKey } from '../api-keys.services';
|
import { createApiKey } from '../api-keys.services';
|
||||||
import { ApiKeyPermissionsPicker } from '../components/api-key-permissions-picker.component';
|
import { ApiKeyPermissionsPicker } from '../components/api-key-permissions-picker.component';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Config } from '../config/config';
|
import type { Config } from '../config/config';
|
||||||
|
import type { SsoProviderConfig } from './auth.types';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
import { ssoProviders } from './auth.constants';
|
import { ssoProviders } from './auth.constants';
|
||||||
|
|
||||||
@@ -8,8 +9,15 @@ export function isAuthErrorWithCode({ error, code }: { error: unknown; code: str
|
|||||||
|
|
||||||
export const isEmailVerificationRequiredError = ({ error }: { error: unknown }) => isAuthErrorWithCode({ error, code: 'EMAIL_NOT_VERIFIED' });
|
export const isEmailVerificationRequiredError = ({ error }: { error: unknown }) => isAuthErrorWithCode({ error, code: 'EMAIL_NOT_VERIFIED' });
|
||||||
|
|
||||||
export function getEnabledSsoProviderConfigs({ config }: { config: Config }) {
|
export function getEnabledSsoProviderConfigs({ config }: { config: Config }): SsoProviderConfig[] {
|
||||||
const enabledSsoProviders = ssoProviders.filter(({ key }) => get(config, `auth.providers.${key}.isEnabled`));
|
const enabledSsoProviders: SsoProviderConfig[] = [
|
||||||
|
...ssoProviders.filter(({ key }) => get(config, `auth.providers.${key}.isEnabled`)),
|
||||||
|
...config.auth.providers.customs.map(({ providerId, providerName, providerIconUrl }) => ({
|
||||||
|
key: providerId,
|
||||||
|
name: providerName,
|
||||||
|
icon: providerIconUrl ?? 'i-tabler-login-2',
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
return enabledSsoProviders;
|
return enabledSsoProviders;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { createAuthClient as createBetterAuthClient } from 'better-auth/solid';
|
import type { Config } from '../config/config';
|
||||||
|
|
||||||
|
import type { SsoProviderConfig } from './auth.types';
|
||||||
|
import { genericOAuthClient } from 'better-auth/client/plugins';
|
||||||
|
import { createAuthClient as createBetterAuthClient } from 'better-auth/solid';
|
||||||
import { buildTimeConfig } from '../config/config';
|
import { buildTimeConfig } from '../config/config';
|
||||||
import { trackingServices } from '../tracking/tracking.services';
|
import { trackingServices } from '../tracking/tracking.services';
|
||||||
import { createDemoAuthClient } from './auth.demo.services';
|
import { createDemoAuthClient } from './auth.demo.services';
|
||||||
@@ -7,6 +10,9 @@ import { createDemoAuthClient } from './auth.demo.services';
|
|||||||
export function createAuthClient() {
|
export function createAuthClient() {
|
||||||
const client = createBetterAuthClient({
|
const client = createBetterAuthClient({
|
||||||
baseURL: buildTimeConfig.baseApiUrl,
|
baseURL: buildTimeConfig.baseApiUrl,
|
||||||
|
plugins: [
|
||||||
|
genericOAuthClient(),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -38,3 +44,17 @@ export const {
|
|||||||
} = buildTimeConfig.isDemoMode
|
} = buildTimeConfig.isDemoMode
|
||||||
? createDemoAuthClient()
|
? createDemoAuthClient()
|
||||||
: createAuthClient();
|
: createAuthClient();
|
||||||
|
|
||||||
|
export async function authWithProvider({ provider, config }: { provider: SsoProviderConfig; config: Config }) {
|
||||||
|
const isCustomProvider = config.auth.providers.customs.some(({ providerId }) => providerId === provider.key);
|
||||||
|
|
||||||
|
if (isCustomProvider) {
|
||||||
|
signIn.oauth2({
|
||||||
|
providerId: provider.key,
|
||||||
|
callbackURL: config.baseUrl,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await signIn.social({ provider: provider.key as 'github' | 'google', callbackURL: config.baseUrl });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import type { ssoProviders } from './auth.constants';
|
import type { ssoProviders } from './auth.constants';
|
||||||
|
|
||||||
export type SsoProviderKey = (typeof ssoProviders)[number]['key'];
|
export type SsoProviderKey = (typeof ssoProviders)[number]['key'] | string & {};
|
||||||
|
export type SsoProviderConfig = { key: SsoProviderKey; name: string; icon: string };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { createVitrineUrl } from '@/modules/shared/utils/urls';
|
import { createVitrineUrl } from '@/modules/shared/utils/urls';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { A } from '@solidjs/router';
|
|
||||||
|
|
||||||
export const AuthLegalLinks: Component = () => {
|
export const AuthLegalLinks: Component = () => {
|
||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Component, ComponentProps } from 'solid-js';
|
import type { Component, ComponentProps } from 'solid-js';
|
||||||
|
import { splitProps } from 'solid-js';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { splitProps } from 'solid-js';
|
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { createSignal, Match, Switch } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { createSignal } from 'solid-js';
|
|
||||||
|
|
||||||
export const SsoProviderButton: Component<{ name: string; icon: string; onClick: () => Promise<void>; label: string }> = (props) => {
|
export const SsoProviderButton: Component<{ name: string; icon?: string; onClick: () => Promise<void>; label: string }> = (props) => {
|
||||||
const [getIsLoading, setIsLoading] = createSignal(false);
|
const [getIsLoading, setIsLoading] = createSignal(false);
|
||||||
|
|
||||||
const navigateToProvider = async () => {
|
const onClick = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await props.onClick();
|
await props.onClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="secondary" class="block w-full flex items-center justify-center" onClick={navigateToProvider} disabled={getIsLoading()}>
|
<Button variant="secondary" class="block w-full flex items-center justify-center gap-2" onClick={onClick} disabled={getIsLoading()}>
|
||||||
<span class={cn(`mr-2 size-4.5 inline-block`, getIsLoading() ? 'i-tabler-loader-2 animate-spin' : props.icon)} />
|
|
||||||
|
<Switch>
|
||||||
|
<Match when={getIsLoading()}>
|
||||||
|
<span class="i-tabler-loader-2 animate-spin" />
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<Match when={props.icon?.startsWith('i-')}>
|
||||||
|
<span class={cn(`size-4.5`, props.icon)} />
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<Match when={props.icon}>
|
||||||
|
<img src={props.icon} alt={props.name} class="size-4.5" />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
{props.label}
|
{props.label}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { SsoProviderKey } from '../auth.types';
|
import type { SsoProviderConfig } from '../auth.types';
|
||||||
|
import { A, useNavigate } from '@solidjs/router';
|
||||||
|
import { createSignal, For, Show } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
@@ -7,12 +10,9 @@ import { Button } from '@/modules/ui/components/button';
|
|||||||
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
import { Checkbox, CheckboxControl, CheckboxLabel } from '@/modules/ui/components/checkbox';
|
||||||
import { Separator } from '@/modules/ui/components/separator';
|
import { Separator } from '@/modules/ui/components/separator';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { A, useNavigate } from '@solidjs/router';
|
|
||||||
import { createSignal, For, Show } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||||
import { getEnabledSsoProviderConfigs, isEmailVerificationRequiredError } from '../auth.models';
|
import { getEnabledSsoProviderConfigs, isEmailVerificationRequiredError } from '../auth.models';
|
||||||
import { signIn } from '../auth.services';
|
import { authWithProvider, signIn } from '../auth.services';
|
||||||
import { AuthLegalLinks } from '../components/legal-links.component';
|
import { AuthLegalLinks } from '../components/legal-links.component';
|
||||||
import { SsoProviderButton } from '../components/sso-provider-button.component';
|
import { SsoProviderButton } from '../components/sso-provider-button.component';
|
||||||
|
|
||||||
@@ -105,8 +105,8 @@ export const LoginPage: Component = () => {
|
|||||||
|
|
||||||
const [getShowEmailLogin, setShowEmailLogin] = createSignal(false);
|
const [getShowEmailLogin, setShowEmailLogin] = createSignal(false);
|
||||||
|
|
||||||
const loginWithProvider = async (provider: { key: SsoProviderKey }) => {
|
const loginWithProvider = async (provider: SsoProviderConfig) => {
|
||||||
await signIn.social({ provider: provider.key, callbackURL: config.baseUrl });
|
await authWithProvider({ provider, config });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
|
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { ssoProviders } from '../auth.constants';
|
import type { SsoProviderConfig } from '../auth.types';
|
||||||
|
import { A, useNavigate } from '@solidjs/router';
|
||||||
|
import { createSignal, For, Show } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { Separator } from '@/modules/ui/components/separator';
|
import { Separator } from '@/modules/ui/components/separator';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { A, useNavigate } from '@solidjs/router';
|
|
||||||
import { createSignal, For, Show } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||||
import { getEnabledSsoProviderConfigs } from '../auth.models';
|
import { getEnabledSsoProviderConfigs } from '../auth.models';
|
||||||
import { signIn, signUp } from '../auth.services';
|
import { authWithProvider, signUp } from '../auth.services';
|
||||||
import { AuthLegalLinks } from '../components/legal-links.component';
|
import { AuthLegalLinks } from '../components/legal-links.component';
|
||||||
import { SsoProviderButton } from '../components/sso-provider-button.component';
|
import { SsoProviderButton } from '../components/sso-provider-button.component';
|
||||||
|
|
||||||
@@ -133,8 +133,8 @@ export const RegisterPage: Component = () => {
|
|||||||
|
|
||||||
const [getShowEmailRegister, setShowEmailRegister] = createSignal(false);
|
const [getShowEmailRegister, setShowEmailRegister] = createSignal(false);
|
||||||
|
|
||||||
const registerWithProvider = async (provider: typeof ssoProviders[number]) => {
|
const registerWithProvider = async (provider: SsoProviderConfig) => {
|
||||||
await signIn.social({ provider: provider.key });
|
await authWithProvider({ provider, config });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
|
const getHasSsoProviders = () => getEnabledSsoProviderConfigs({ config }).length > 0;
|
||||||
@@ -169,7 +169,7 @@ export const RegisterPage: Component = () => {
|
|||||||
name={provider.name}
|
name={provider.name}
|
||||||
icon={provider.icon}
|
icon={provider.icon}
|
||||||
onClick={() => registerWithProvider(provider)}
|
onClick={() => registerWithProvider(provider)}
|
||||||
label={t('auth.register.register-with-provider', { provider: t(`auth.register.providers.${provider.key}`) })}
|
label={t('auth.register.register-with-provider', { provider: provider.name })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { buildUrl } from '@corentinth/chisels';
|
||||||
|
import { A, useNavigate } from '@solidjs/router';
|
||||||
|
import { createSignal, onMount } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { buildUrl } from '@corentinth/chisels';
|
|
||||||
import { A, useNavigate } from '@solidjs/router';
|
|
||||||
import { createSignal, onMount } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||||
import { forgetPassword } from '../auth.services';
|
import { forgetPassword } from '../auth.services';
|
||||||
import { OpenEmailProvider } from '../components/open-email-provider.component';
|
import { OpenEmailProvider } from '../components/open-email-provider.component';
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { A, Navigate, useNavigate, useSearchParams } from '@solidjs/router';
|
||||||
|
import { createSignal, onMount } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { A, Navigate, useNavigate, useSearchParams } from '@solidjs/router';
|
|
||||||
import { createSignal, onMount } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
import { AuthLayout } from '../../ui/layouts/auth-layout.component';
|
||||||
import { resetPassword } from '../auth.services';
|
import { resetPassword } from '../auth.services';
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const buildTimeConfig = {
|
|||||||
providers: {
|
providers: {
|
||||||
github: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GITHUB_IS_ENABLED, false) },
|
github: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GITHUB_IS_ENABLED, false) },
|
||||||
google: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GOOGLE_IS_ENABLED, false) },
|
google: { isEnabled: asBoolean(import.meta.env.VITE_AUTH_PROVIDERS_GOOGLE_IS_ENABLED, false) },
|
||||||
|
customs: [] as {
|
||||||
|
providerId: string;
|
||||||
|
providerName: string;
|
||||||
|
providerIconUrl: string;
|
||||||
|
}[],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
documents: {
|
documents: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ApiKey } from '../api-keys/api-keys.types';
|
import type { ApiKey } from '../api-keys/api-keys.types';
|
||||||
|
import type { Webhook } from '../webhooks/webhooks.types';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
import { FetchError } from 'ofetch';
|
import { FetchError } from 'ofetch';
|
||||||
import { createRouter } from 'radix3';
|
import { createRouter } from 'radix3';
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
tagDocumentStorage,
|
tagDocumentStorage,
|
||||||
taggingRuleStorage,
|
taggingRuleStorage,
|
||||||
tagStorage,
|
tagStorage,
|
||||||
|
webhooksStorage,
|
||||||
} from './demo.storage';
|
} from './demo.storage';
|
||||||
import { findMany, getValues } from './demo.storage.models';
|
import { findMany, getValues } from './demo.storage.models';
|
||||||
|
|
||||||
@@ -565,6 +567,55 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/members',
|
||||||
|
method: 'GET',
|
||||||
|
handler: async ({ params: { organizationId } }) => {
|
||||||
|
return {
|
||||||
|
members: [{
|
||||||
|
id: 'mem_1',
|
||||||
|
user: {
|
||||||
|
id: 'usr_1',
|
||||||
|
email: 'jane.doe@papra.app',
|
||||||
|
name: 'Jane Doe',
|
||||||
|
},
|
||||||
|
role: 'owner',
|
||||||
|
organizationId,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/members/invitations',
|
||||||
|
method: 'POST',
|
||||||
|
handler: async () => {
|
||||||
|
throw Object.assign(new FetchError('Not available in demo'), {
|
||||||
|
status: 501,
|
||||||
|
data: {
|
||||||
|
error: {
|
||||||
|
message: 'This feature is not available in demo',
|
||||||
|
code: 'demo.not_available',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/members/me',
|
||||||
|
method: 'GET',
|
||||||
|
handler: async ({ params: { organizationId } }) => {
|
||||||
|
return {
|
||||||
|
member: {
|
||||||
|
id: 'mem_1',
|
||||||
|
role: 'owner',
|
||||||
|
organizationId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
...defineHandler({
|
...defineHandler({
|
||||||
path: '/api/api-keys',
|
path: '/api/api-keys',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -606,6 +657,80 @@ const inMemoryApiMock: Record<string, { handler: any }> = {
|
|||||||
await apiKeyStorage.removeItem(apiKeyId);
|
await apiKeyStorage.removeItem(apiKeyId);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/invitations/count',
|
||||||
|
method: 'GET',
|
||||||
|
handler: async () => ({ pendingInvitationsCount: 0 }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/invitations',
|
||||||
|
method: 'GET',
|
||||||
|
handler: async () => ({ invitations: [] }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/webhooks',
|
||||||
|
method: 'GET',
|
||||||
|
handler: async ({ params: { organizationId } }) => {
|
||||||
|
const webhooks = await findMany(webhooksStorage, webhook => webhook.organizationId === organizationId);
|
||||||
|
|
||||||
|
return { webhooks };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/webhooks',
|
||||||
|
method: 'POST',
|
||||||
|
handler: async ({ params: { organizationId }, body }) => {
|
||||||
|
const webhook: Webhook = {
|
||||||
|
id: createId({ prefix: 'webhook' }),
|
||||||
|
organizationId,
|
||||||
|
name: get(body, 'name'),
|
||||||
|
url: get(body, 'url'),
|
||||||
|
enabled: true,
|
||||||
|
events: get(body, 'events'),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await webhooksStorage.setItem(webhook.id, webhook);
|
||||||
|
|
||||||
|
return { webhook };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/webhooks/:webhookId',
|
||||||
|
method: 'GET',
|
||||||
|
handler: async ({ params: { webhookId } }) => {
|
||||||
|
const webhook = await webhooksStorage.getItem(webhookId);
|
||||||
|
return { webhook };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/webhooks/:webhookId',
|
||||||
|
method: 'DELETE',
|
||||||
|
handler: async ({ params: { webhookId } }) => {
|
||||||
|
await webhooksStorage.removeItem(webhookId);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...defineHandler({
|
||||||
|
path: '/api/organizations/:organizationId/webhooks/:webhookId',
|
||||||
|
method: 'PUT',
|
||||||
|
handler: async ({ params: { webhookId }, body }) => {
|
||||||
|
const webhook = await webhooksStorage.getItem(webhookId);
|
||||||
|
|
||||||
|
assert(webhook, { status: 404 });
|
||||||
|
|
||||||
|
await webhooksStorage.setItem(webhookId, Object.assign(webhook, body, { updatedAt: new Date() }));
|
||||||
|
|
||||||
|
return { webhook };
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const router = createRouter({ routes: inMemoryApiMock, strictTrailingSlash: false });
|
export const router = createRouter({ routes: inMemoryApiMock, strictTrailingSlash: false });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Document } from '../documents/documents.types';
|
|||||||
import type { Organization } from '../organizations/organizations.types';
|
import type { Organization } from '../organizations/organizations.types';
|
||||||
import type { TaggingRule } from '../tagging-rules/tagging-rules.types';
|
import type { TaggingRule } from '../tagging-rules/tagging-rules.types';
|
||||||
import type { Tag } from '../tags/tags.types';
|
import type { Tag } from '../tags/tags.types';
|
||||||
|
import type { Webhook } from '../webhooks/webhooks.types';
|
||||||
import { createStorage, prefixStorage } from 'unstorage';
|
import { createStorage, prefixStorage } from 'unstorage';
|
||||||
import localStorageDriver from 'unstorage/drivers/localstorage';
|
import localStorageDriver from 'unstorage/drivers/localstorage';
|
||||||
import { trackingServices } from '../tracking/tracking.services';
|
import { trackingServices } from '../tracking/tracking.services';
|
||||||
@@ -18,6 +19,7 @@ export const tagStorage = prefixStorage<Omit<Tag, 'documentsCount'>>(storage, 't
|
|||||||
export const tagDocumentStorage = prefixStorage<{ documentId: string; tagId: string; id: string }>(storage, 'tagDocuments');
|
export const tagDocumentStorage = prefixStorage<{ documentId: string; tagId: string; id: string }>(storage, 'tagDocuments');
|
||||||
export const taggingRuleStorage = prefixStorage<TaggingRule>(storage, 'taggingRules');
|
export const taggingRuleStorage = prefixStorage<TaggingRule>(storage, 'taggingRules');
|
||||||
export const apiKeyStorage = prefixStorage<ApiKey>(storage, 'apiKeys');
|
export const apiKeyStorage = prefixStorage<ApiKey>(storage, 'apiKeys');
|
||||||
|
export const webhooksStorage = prefixStorage<Webhook>(storage, 'webhooks');
|
||||||
|
|
||||||
export async function clearDemoStorage() {
|
export async function clearDemoStorage() {
|
||||||
await storage.clear();
|
await storage.clear();
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { ParentComponent } from 'solid-js';
|
import type { ParentComponent } from 'solid-js';
|
||||||
import type { Document } from '../documents.types';
|
import type { Document } from '../documents.types';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
|
||||||
import { promptUploadFiles } from '@/modules/shared/files/upload';
|
|
||||||
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Button } from '@/modules/ui/components/button';
|
|
||||||
import { safely } from '@corentinth/chisels';
|
import { safely } from '@corentinth/chisels';
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
import { createContext, createSignal, For, Match, Show, Switch, useContext } from 'solid-js';
|
import { createContext, createSignal, For, Match, Show, Switch, useContext } from 'solid-js';
|
||||||
import { Portal } from 'solid-js/web';
|
import { Portal } from 'solid-js/web';
|
||||||
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
|
import { promptUploadFiles } from '@/modules/shared/files/upload';
|
||||||
|
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { invalidateOrganizationDocumentsQuery } from '../documents.composables';
|
import { invalidateOrganizationDocumentsQuery } from '../documents.composables';
|
||||||
import { uploadDocument } from '../documents.services';
|
import { uploadDocument } from '../documents.services';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const DocumentUploadContext = createContext<{
|
|||||||
uploadDocuments: (args: { files: File[]; organizationId: string }) => Promise<void>;
|
uploadDocuments: (args: { files: File[]; organizationId: string }) => Promise<void>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export function useDocumentUpload({ organizationId }: { organizationId: string }) {
|
export function useDocumentUpload({ getOrganizationId }: { getOrganizationId: () => string }) {
|
||||||
const context = useContext(DocumentUploadContext);
|
const context = useContext(DocumentUploadContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -27,11 +27,11 @@ export function useDocumentUpload({ organizationId }: { organizationId: string }
|
|||||||
const { uploadDocuments } = context;
|
const { uploadDocuments } = context;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uploadDocuments: async ({ files }: { files: File[] }) => uploadDocuments({ files, organizationId }),
|
uploadDocuments: async ({ files }: { files: File[] }) => uploadDocuments({ files, organizationId: getOrganizationId() }),
|
||||||
promptImport: async () => {
|
promptImport: async () => {
|
||||||
const { files } = await promptUploadFiles();
|
const { files } = await promptUploadFiles();
|
||||||
|
|
||||||
await uploadDocuments({ files, organizationId });
|
await uploadDocuments({ files, organizationId: getOrganizationId() });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { DropdownMenuSubTriggerProps } from '@kobalte/core/dropdown-menu';
|
import type { DropdownMenuSubTriggerProps } from '@kobalte/core/dropdown-menu';
|
||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { Document } from '../documents.types';
|
import type { Document } from '../documents.types';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/modules/ui/components/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/modules/ui/components/dropdown-menu';
|
||||||
import { A } from '@solidjs/router';
|
|
||||||
import { useDeleteDocument } from '../documents.composables';
|
import { useDeleteDocument } from '../documents.composables';
|
||||||
|
|
||||||
export const DocumentManagementDropdown: Component<{ document: Document }> = (props) => {
|
export const DocumentManagementDropdown: Component<{ document: Document }> = (props) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { Document } from '../documents.types';
|
import type { Document } from '../documents.types';
|
||||||
import { Card } from '@/modules/ui/components/card';
|
|
||||||
import { createQuery } from '@tanstack/solid-query';
|
import { createQuery } from '@tanstack/solid-query';
|
||||||
import { createResource, Match, Suspense, Switch } from 'solid-js';
|
import { createResource, Match, Suspense, Switch } from 'solid-js';
|
||||||
|
import { Card } from '@/modules/ui/components/card';
|
||||||
import { fetchDocumentFile } from '../documents.services';
|
import { fetchDocumentFile } from '../documents.services';
|
||||||
import { PdfViewer } from './pdf-viewer.component';
|
import { PdfViewer } from './pdf-viewer.component';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { useParams } from '@solidjs/router';
|
||||||
|
import { createSignal } from 'solid-js';
|
||||||
import { promptUploadFiles } from '@/modules/shared/files/upload';
|
import { promptUploadFiles } from '@/modules/shared/files/upload';
|
||||||
import { queryClient } from '@/modules/shared/query/query-client';
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { useParams } from '@solidjs/router';
|
|
||||||
import { createSignal } from 'solid-js';
|
|
||||||
import { uploadDocument } from '../documents.services';
|
import { uploadDocument } from '../documents.services';
|
||||||
|
|
||||||
export const DocumentUploadArea: Component<{ organizationId?: string }> = (props) => {
|
export const DocumentUploadArea: Component<{ organizationId?: string }> = (props) => {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import type { Tag } from '@/modules/tags/tags.types';
|
|
||||||
import type { TooltipTriggerProps } from '@kobalte/core/tooltip';
|
import type { TooltipTriggerProps } from '@kobalte/core/tooltip';
|
||||||
import type { ColumnDef } from '@tanstack/solid-table';
|
import type { ColumnDef } from '@tanstack/solid-table';
|
||||||
import type { Accessor, Component, Setter } from 'solid-js';
|
import type { Accessor, Component, Setter } from 'solid-js';
|
||||||
import type { Document } from '../documents.types';
|
import type { Document } from '../documents.types';
|
||||||
|
import type { Tag } from '@/modules/tags/tags.types';
|
||||||
|
import { formatBytes } from '@corentinth/chisels';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
|
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
|
||||||
|
import { For, Match, Show, Switch } from 'solid-js';
|
||||||
import { timeAgo } from '@/modules/shared/date/time-ago';
|
import { timeAgo } from '@/modules/shared/date/time-ago';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
import { TagLink } from '@/modules/tags/components/tag.component';
|
import { TagLink } from '@/modules/tags/components/tag.component';
|
||||||
@@ -10,10 +14,6 @@ import { Button } from '@/modules/ui/components/button';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/modules/ui/components/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/modules/ui/components/select';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/modules/ui/components/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/modules/ui/components/table';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/modules/ui/components/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/modules/ui/components/tooltip';
|
||||||
import { formatBytes } from '@corentinth/chisels';
|
|
||||||
import { A } from '@solidjs/router';
|
|
||||||
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
|
|
||||||
import { For, Match, Show, Switch } from 'solid-js';
|
|
||||||
import { getDocumentIcon, getDocumentNameExtension, getDocumentNameWithoutExtension } from '../document.models';
|
import { getDocumentIcon, getDocumentNameExtension, getDocumentNameWithoutExtension } from '../document.models';
|
||||||
import { DocumentManagementDropdown } from './document-management-dropdown.component';
|
import { DocumentManagementDropdown } from './document-management-dropdown.component';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { createSignal, onCleanup } from 'solid-js';
|
import { createSignal, onCleanup } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const GlobalDropArea: Component<{ onFilesDrop?: (args: { files: File[] }) => void }> = (props) => {
|
export const GlobalDropArea: Component<{ onFilesDrop?: (args: { files: File[] }) => void }> = (props) => {
|
||||||
const [isDragging, setIsDragging] = createSignal(false);
|
const [isDragging, setIsDragging] = createSignal(false);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { Document } from '../documents.types';
|
import type { Document } from '../documents.types';
|
||||||
|
import { useParams } from '@solidjs/router';
|
||||||
|
import { createMutation, createQuery, keepPreviousData } from '@tanstack/solid-query';
|
||||||
|
import { createSignal, Show, Suspense } from 'solid-js';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||||
@@ -8,9 +11,6 @@ import { queryClient } from '@/modules/shared/query/query-client';
|
|||||||
import { Alert, AlertDescription } from '@/modules/ui/components/alert';
|
import { Alert, AlertDescription } from '@/modules/ui/components/alert';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { useParams } from '@solidjs/router';
|
|
||||||
import { createMutation, createQuery, keepPreviousData } from '@tanstack/solid-query';
|
|
||||||
import { createSignal, Show, Suspense } from 'solid-js';
|
|
||||||
import { DocumentsPaginatedList } from '../components/documents-list.component';
|
import { DocumentsPaginatedList } from '../components/documents-list.component';
|
||||||
import { useRestoreDocument } from '../documents.composables';
|
import { useRestoreDocument } from '../documents.composables';
|
||||||
import { deleteAllTrashDocuments, deleteTrashDocument, fetchOrganizationDeletedDocuments } from '../documents.services';
|
import { deleteAllTrashDocuments, deleteTrashDocument, fetchOrganizationDeletedDocuments } from '../documents.services';
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { Component, JSX } from 'solid-js';
|
import type { Component, JSX } from 'solid-js';
|
||||||
|
import { formatBytes, safely } from '@corentinth/chisels';
|
||||||
|
import { useNavigate, useParams } from '@solidjs/router';
|
||||||
|
import { createQueries } from '@tanstack/solid-query';
|
||||||
|
import { createSignal, For, Show, Suspense } from 'solid-js';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { timeAgo } from '@/modules/shared/date/time-ago';
|
import { timeAgo } from '@/modules/shared/date/time-ago';
|
||||||
@@ -14,10 +18,6 @@ import { createToast } from '@/modules/ui/components/sonner';
|
|||||||
import { Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger } from '@/modules/ui/components/tabs';
|
import { Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger } from '@/modules/ui/components/tabs';
|
||||||
import { TextArea } from '@/modules/ui/components/textarea';
|
import { TextArea } from '@/modules/ui/components/textarea';
|
||||||
import { TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { formatBytes, safely } from '@corentinth/chisels';
|
|
||||||
import { useNavigate, useParams } from '@solidjs/router';
|
|
||||||
import { createQueries } from '@tanstack/solid-query';
|
|
||||||
import { createSignal, For, Show, Suspense } from 'solid-js';
|
|
||||||
import { DocumentPreview } from '../components/document-preview.component';
|
import { DocumentPreview } from '../components/document-preview.component';
|
||||||
import { getDaysBeforePermanentDeletion } from '../document.models';
|
import { getDaysBeforePermanentDeletion } from '../document.models';
|
||||||
import { useDeleteDocument, useRestoreDocument } from '../documents.composables';
|
import { useDeleteDocument, useRestoreDocument } from '../documents.composables';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { fetchOrganization } from '@/modules/organizations/organizations.services';
|
|
||||||
import { Tag } from '@/modules/tags/components/tag.component';
|
|
||||||
import { fetchTags } from '@/modules/tags/tags.services';
|
|
||||||
import { useParams, useSearchParams } from '@solidjs/router';
|
import { useParams, useSearchParams } from '@solidjs/router';
|
||||||
import { createQueries, keepPreviousData } from '@tanstack/solid-query';
|
import { createQueries, keepPreviousData } from '@tanstack/solid-query';
|
||||||
import { castArray } from 'lodash-es';
|
import { castArray } from 'lodash-es';
|
||||||
import { createSignal, For, Show, Suspense } from 'solid-js';
|
import { createSignal, For, Show, Suspense } from 'solid-js';
|
||||||
|
import { fetchOrganization } from '@/modules/organizations/organizations.services';
|
||||||
|
import { Tag } from '@/modules/tags/components/tag.component';
|
||||||
|
import { fetchTags } from '@/modules/tags/tags.services';
|
||||||
import { DocumentUploadArea } from '../components/document-upload-area.component';
|
import { DocumentUploadArea } from '../components/document-upload-area.component';
|
||||||
import { createdAtColumn, DocumentsPaginatedList, standardActionsColumn, tagsColumn } from '../components/documents-list.component';
|
import { createdAtColumn, DocumentsPaginatedList, standardActionsColumn, tagsColumn } from '../components/documents-list.component';
|
||||||
import { fetchOrganizationDocuments } from '../documents.services';
|
import { fetchOrganizationDocuments } from '../documents.services';
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ describe('locales', () => {
|
|||||||
/^auth\.register\.providers\.[a-z0-9:]+$/, // auth.register.providers.google
|
/^auth\.register\.providers\.[a-z0-9:]+$/, // auth.register.providers.google
|
||||||
/^webhooks\.events\.documents\.[a-z0-9:]+.description$/, // webhooks.events.organization.organization:created
|
/^webhooks\.events\.documents\.[a-z0-9:]+.description$/, // webhooks.events.organization.organization:created
|
||||||
/^api-keys\.permissions\.[a-z0-9:]+\.[a-z0-9:]+$/, // api-keys.permissions.documents.documents:delete
|
/^api-keys\.permissions\.[a-z0-9:]+\.[a-z0-9:]+$/, // api-keys.permissions.documents.documents:delete
|
||||||
|
/^organizations\.members\.roles\.[a-z0-9]+$/, // organizations.members.roles.admin
|
||||||
];
|
];
|
||||||
|
|
||||||
const keys = new Set(
|
const keys = new Set(
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ export type LocaleKeys =
|
|||||||
| 'layout.menu.general-settings'
|
| 'layout.menu.general-settings'
|
||||||
| 'layout.menu.intake-emails'
|
| 'layout.menu.intake-emails'
|
||||||
| 'layout.menu.webhooks'
|
| 'layout.menu.webhooks'
|
||||||
|
| 'layout.menu.members'
|
||||||
|
| 'layout.menu.invitations'
|
||||||
| 'tagging-rules.field.name'
|
| 'tagging-rules.field.name'
|
||||||
| 'tagging-rules.field.content'
|
| 'tagging-rules.field.content'
|
||||||
| 'tagging-rules.operator.equals'
|
| 'tagging-rules.operator.equals'
|
||||||
@@ -158,6 +160,10 @@ export type LocaleKeys =
|
|||||||
| 'api-errors.intake_email.limit_reached'
|
| 'api-errors.intake_email.limit_reached'
|
||||||
| 'api-errors.user.max_organization_count_reached'
|
| 'api-errors.user.max_organization_count_reached'
|
||||||
| 'api-errors.default'
|
| 'api-errors.default'
|
||||||
|
| 'api-errors.organization.invitation_already_exists'
|
||||||
|
| 'api-errors.user.already_in_organization'
|
||||||
|
| 'api-errors.user.organization_invitation_limit_reached'
|
||||||
|
| 'api-errors.demo.not_available'
|
||||||
| 'api-keys.permissions.documents.title'
|
| 'api-keys.permissions.documents.title'
|
||||||
| 'api-keys.permissions.documents.documents:create'
|
| 'api-keys.permissions.documents.documents:create'
|
||||||
| 'api-keys.permissions.documents.documents:read'
|
| 'api-keys.permissions.documents.documents:read'
|
||||||
@@ -231,4 +237,42 @@ export type LocaleKeys =
|
|||||||
| 'webhooks.delete.confirm.confirm-button'
|
| 'webhooks.delete.confirm.confirm-button'
|
||||||
| 'webhooks.delete.confirm.cancel-button'
|
| 'webhooks.delete.confirm.cancel-button'
|
||||||
| 'webhooks.events.documents.document:created.description'
|
| 'webhooks.events.documents.document:created.description'
|
||||||
| 'webhooks.events.documents.document:deleted.description';
|
| 'webhooks.events.documents.document:deleted.description'
|
||||||
|
| 'organizations.members.title'
|
||||||
|
| 'organizations.members.description'
|
||||||
|
| 'organizations.members.invite-member'
|
||||||
|
| 'organizations.members.invite-member-disabled-tooltip'
|
||||||
|
| 'organizations.members.remove-from-organization'
|
||||||
|
| 'organizations.members.role'
|
||||||
|
| 'organizations.members.roles.owner'
|
||||||
|
| 'organizations.members.roles.admin'
|
||||||
|
| 'organizations.members.roles.member'
|
||||||
|
| 'organizations.members.delete.confirm.title'
|
||||||
|
| 'organizations.members.delete.confirm.message'
|
||||||
|
| 'organizations.members.delete.confirm.confirm-button'
|
||||||
|
| 'organizations.members.delete.confirm.cancel-button'
|
||||||
|
| 'organizations.members.delete.success'
|
||||||
|
| 'organizations.members.update-role.success'
|
||||||
|
| 'organizations.invite-member.title'
|
||||||
|
| 'organizations.invite-member.description'
|
||||||
|
| 'organizations.invite-member.form.email.label'
|
||||||
|
| 'organizations.invite-member.form.email.placeholder'
|
||||||
|
| 'organizations.invite-member.form.email.required'
|
||||||
|
| 'organizations.invite-member.form.role.label'
|
||||||
|
| 'organizations.invite-member.form.submit'
|
||||||
|
| 'organizations.invite-member.success.message'
|
||||||
|
| 'organizations.invite-member.success.description'
|
||||||
|
| 'organizations.invite-member.error.message'
|
||||||
|
| 'invitations.list.title'
|
||||||
|
| 'invitations.list.description'
|
||||||
|
| 'invitations.list.empty.title'
|
||||||
|
| 'invitations.list.empty.description'
|
||||||
|
| 'invitations.list.headers.organization'
|
||||||
|
| 'invitations.list.headers.created'
|
||||||
|
| 'invitations.list.headers.actions'
|
||||||
|
| 'invitations.list.actions.accept'
|
||||||
|
| 'invitations.list.actions.reject'
|
||||||
|
| 'invitations.list.actions.accept.success.message'
|
||||||
|
| 'invitations.list.actions.accept.success.description'
|
||||||
|
| 'invitations.list.actions.reject.success.message'
|
||||||
|
| 'invitations.list.actions.reject.success.description';
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { DialogTriggerProps } from '@kobalte/core/dialog';
|
import type { DialogTriggerProps } from '@kobalte/core/dialog';
|
||||||
import type { Component, JSX } from 'solid-js';
|
import type { Component, JSX } from 'solid-js';
|
||||||
import type { IntakeEmail } from '../intake-emails.types';
|
import type { IntakeEmail } from '../intake-emails.types';
|
||||||
|
import { safely } from '@corentinth/chisels';
|
||||||
|
import { useParams } from '@solidjs/router';
|
||||||
|
import { createQuery } from '@tanstack/solid-query';
|
||||||
|
import { createSignal, For, Show, Suspense } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
@@ -14,11 +19,6 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
|
|||||||
import { EmptyState } from '@/modules/ui/components/empty';
|
import { EmptyState } from '@/modules/ui/components/empty';
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { safely } from '@corentinth/chisels';
|
|
||||||
import { useParams } from '@solidjs/router';
|
|
||||||
import { createQuery } from '@tanstack/solid-query';
|
|
||||||
import { createSignal, For, Show, Suspense } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { createIntakeEmail, deleteIntakeEmail, fetchIntakeEmails, updateIntakeEmail } from '../intake-emails.services';
|
import { createIntakeEmail, deleteIntakeEmail, fetchIntakeEmails, updateIntakeEmail } from '../intake-emails.services';
|
||||||
|
|
||||||
const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) => JSX.Element; intakeEmails: IntakeEmail }> = (props) => {
|
const AllowedOriginsDialog: Component<{ children: (props: DialogTriggerProps) => JSX.Element; intakeEmails: IntakeEmail }> = (props) => {
|
||||||
@@ -133,12 +133,27 @@ export const IntakeEmailsPage: Component = () => {
|
|||||||
|
|
||||||
if (!config.intakeEmails.isEnabled) {
|
if (!config.intakeEmails.isEnabled) {
|
||||||
return (
|
return (
|
||||||
<Card class="p-6">
|
<div class="p-6 max-w-screen-md mx-auto mt-10">
|
||||||
<h2 class="text-base font-bold">Intake Emails</h2>
|
|
||||||
|
<h1 class="text-xl font-semibold">Intake Emails</h1>
|
||||||
|
|
||||||
<p class="text-muted-foreground mt-1">
|
<p class="text-muted-foreground mt-1">
|
||||||
Intake emails are disabled on this instance. Please contact your administrator to enable them.
|
Intake emails address are used to automatically ingest emails into Papra. Just forward emails to the intake email address and their attachments will be added to your organization's documents.
|
||||||
</p>
|
</p>
|
||||||
</Card>
|
<Card class="px-6 py-4 mt-4 flex items-center gap-4">
|
||||||
|
<div class="i-tabler-mail-off size-12 text-muted-foreground flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-bold text-muted-foreground">Intake Emails are disabled</h2>
|
||||||
|
<p class="text-muted-foreground mt-1">
|
||||||
|
Intake emails are disabled on this instance. Please contact your administrator to enable them. See the
|
||||||
|
{' '}
|
||||||
|
<a href="https://docs.papra.app/guides/intake-emails-with-owlrelay/" target="_blank" class="text-primary">documentation</a>
|
||||||
|
{' '}
|
||||||
|
for more information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from '@tanstack/solid-query';
|
||||||
|
import { fetchPendingInvitationsCount } from '../invitations.services';
|
||||||
|
|
||||||
|
export function usePendingInvitationsCount() {
|
||||||
|
const query = useQuery(() => ({
|
||||||
|
queryKey: ['invitations', 'count'],
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
queryFn: fetchPendingInvitationsCount,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
getPendingInvitationsCount: () => query.data?.pendingInvitationsCount ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Organization } from '../organizations/organizations.types';
|
||||||
|
import { apiClient } from '../shared/http/api-client';
|
||||||
|
import { coerceDates } from '../shared/http/http-client.models';
|
||||||
|
|
||||||
|
export async function fetchInvitations() {
|
||||||
|
const { invitations } = await apiClient<{ invitations: { id: string; organization: Organization }[] }>({
|
||||||
|
path: '/api/invitations',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
invitations: invitations.map(i => ({
|
||||||
|
...coerceDates(i),
|
||||||
|
organization: coerceDates(i.organization),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPendingInvitationsCount() {
|
||||||
|
const { pendingInvitationsCount } = await apiClient<{ pendingInvitationsCount: number }>({
|
||||||
|
path: '/api/invitations/count',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
return { pendingInvitationsCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelInvitation({ invitationId }: { invitationId: string }) {
|
||||||
|
await apiClient({
|
||||||
|
path: `/api/invitations/${invitationId}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function acceptInvitation({ invitationId }: { invitationId: string }) {
|
||||||
|
await apiClient({
|
||||||
|
path: `/api/invitations/${invitationId}/accept`,
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rejectInvitation({ invitationId }: { invitationId: string }) {
|
||||||
|
await apiClient({
|
||||||
|
path: `/api/invitations/${invitationId}/reject`,
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { useMutation, useQuery } from '@tanstack/solid-query';
|
||||||
|
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
|
||||||
|
import { For, Show } from 'solid-js';
|
||||||
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
|
import { timeAgo } from '@/modules/shared/date/time-ago';
|
||||||
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
|
import { Button } from '@/modules/ui/components/button';
|
||||||
|
import { EmptyState } from '@/modules/ui/components/empty';
|
||||||
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/modules/ui/components/table';
|
||||||
|
import { acceptInvitation, fetchInvitations, rejectInvitation } from '../invitations.services';
|
||||||
|
|
||||||
|
export const InvitationsPage: Component = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const query = useQuery(() => ({
|
||||||
|
queryKey: ['invitations'],
|
||||||
|
queryFn: fetchInvitations,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const acceptInvitationMutation = useMutation(() => ({
|
||||||
|
mutationFn: acceptInvitation,
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['invitations'] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['organizations'] });
|
||||||
|
|
||||||
|
createToast({
|
||||||
|
message: t('invitations.list.actions.accept.success.message'),
|
||||||
|
description: t('invitations.list.actions.accept.success.description'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const rejectInvitationMutation = useMutation(() => ({
|
||||||
|
mutationFn: rejectInvitation,
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['invitations'] });
|
||||||
|
|
||||||
|
createToast({
|
||||||
|
message: t('invitations.list.actions.reject.success.message'),
|
||||||
|
description: t('invitations.list.actions.reject.success.description'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const table = createSolidTable({
|
||||||
|
get data() {
|
||||||
|
return query.data?.invitations ?? [];
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
header: t('invitations.list.headers.organization'),
|
||||||
|
accessorKey: 'organization.name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: t('invitations.list.headers.created'),
|
||||||
|
accessorKey: 'createdAt',
|
||||||
|
cell: data => <time dateTime={data.getValue()}>{timeAgo({ date: data.getValue() })}</time>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: () => <div class="text-right">{t('invitations.list.headers.actions')}</div>,
|
||||||
|
id: 'actions',
|
||||||
|
cell: data => (
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<Button size="sm" onClick={() => acceptInvitationMutation.mutate({ invitationId: data.row.original.id })}>
|
||||||
|
{t('invitations.list.actions.accept')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="destructive" onClick={() => rejectInvitationMutation.mutate({ invitationId: data.row.original.id })}>
|
||||||
|
{t('invitations.list.actions.reject')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div class="p-6 mt-12 pb-32 mx-auto max-w-xl w-full">
|
||||||
|
<div class="border-b pb-4 mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold mb-1">{t('invitations.list.title')}</h1>
|
||||||
|
<p class="text-muted-foreground">{t('invitations.list.description')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={query.data?.invitations.length} fallback={<EmptyState title={t('invitations.list.empty.title')} icon="i-tabler-mail" description={t('invitations.list.empty.description')} />}>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<For each={table.getHeaderGroups()}>
|
||||||
|
{headerGroup => (
|
||||||
|
<TableRow>
|
||||||
|
<For each={headerGroup.headers}>{header => <TableHead>{flexRender(header.column.columnDef.header, header.getContext())}</TableHead>}</For>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<For each={table.getRowModel().rows}>
|
||||||
|
{row => <TableRow>{row.getVisibleCells().map(cell => <TableCell>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>)}</TableRow>}
|
||||||
|
</For>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { safely } from '@corentinth/chisels';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
import { isHttpErrorWithCode } from '@/modules/shared/http/http-errors';
|
import { isHttpErrorWithCode } from '@/modules/shared/http/http-errors';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { safely } from '@corentinth/chisels';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { organizationNameSchema } from '../organizations.schemas';
|
import { organizationNameSchema } from '../organizations.schemas';
|
||||||
|
|
||||||
export const CreateOrganizationForm: Component<{
|
export const CreateOrganizationForm: Component<{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { useNavigate, useParams } from '@solidjs/router';
|
||||||
|
import { useQuery } from '@tanstack/solid-query';
|
||||||
import { queryClient } from '@/modules/shared/query/query-client';
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { ORGANIZATION_ROLES } from './organizations.constants';
|
||||||
import { createOrganization, deleteOrganization, updateOrganization } from './organizations.services';
|
import { createOrganization, deleteOrganization, getMembership, updateOrganization } from './organizations.services';
|
||||||
|
|
||||||
export function useCreateOrganization() {
|
export function useCreateOrganization() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -50,3 +52,29 @@ export function useDeleteOrganization() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCurrentUserRole({ organizationId }: { organizationId?: string } = {}) {
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const getOrganizationId = () => organizationId ?? params.organizationId;
|
||||||
|
|
||||||
|
const query = useQuery(() => ({
|
||||||
|
queryKey: ['organizations', getOrganizationId(), 'members', 'me'],
|
||||||
|
queryFn: () => getMembership({ organizationId: getOrganizationId() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getRole = () => query.data?.member.role;
|
||||||
|
const getIsMember = () => getRole() === ORGANIZATION_ROLES.MEMBER;
|
||||||
|
const getIsAdmin = () => getRole() === ORGANIZATION_ROLES.ADMIN;
|
||||||
|
const getIsOwner = () => getRole() === ORGANIZATION_ROLES.OWNER;
|
||||||
|
const getIsAtLeastAdmin = () => getIsAdmin() || getIsOwner();
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
getRole,
|
||||||
|
getIsMember,
|
||||||
|
getIsAdmin,
|
||||||
|
getIsOwner,
|
||||||
|
getIsAtLeastAdmin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export const ORGANIZATION_ROLES = {
|
||||||
|
OWNER: 'owner',
|
||||||
|
ADMIN: 'admin',
|
||||||
|
MEMBER: 'member',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ORGANIZATION_ROLES_LIST = Object.values(ORGANIZATION_ROLES);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { OrganizationMemberRole } from './organizations.types';
|
||||||
|
import { ORGANIZATION_ROLES } from './organizations.constants';
|
||||||
|
|
||||||
|
export function getIsMemberRoleDisabled({
|
||||||
|
currentUserRole,
|
||||||
|
memberRole,
|
||||||
|
targetRole,
|
||||||
|
}: {
|
||||||
|
currentUserRole?: OrganizationMemberRole;
|
||||||
|
memberRole: OrganizationMemberRole;
|
||||||
|
targetRole: OrganizationMemberRole;
|
||||||
|
}) {
|
||||||
|
if (currentUserRole === ORGANIZATION_ROLES.MEMBER) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberRole === ORGANIZATION_ROLES.OWNER) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRole === ORGANIZATION_ROLES.OWNER) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
import type { AsDto } from '../shared/http/http-client.types';
|
import type { AsDto } from '../shared/http/http-client.types';
|
||||||
import type { Organization } from './organizations.types';
|
import type { Organization, OrganizationMember, OrganizationMemberRole } from './organizations.types';
|
||||||
import { apiClient } from '../shared/http/api-client';
|
import { apiClient } from '../shared/http/api-client';
|
||||||
import { coerceDates } from '../shared/http/http-client.models';
|
import { coerceDates } from '../shared/http/http-client.models';
|
||||||
|
|
||||||
|
export async function inviteOrganizationMember({ organizationId, email, role }: { organizationId: string; email: string; role: OrganizationMemberRole }) {
|
||||||
|
await apiClient({
|
||||||
|
path: `/api/organizations/${organizationId}/members/invitations`,
|
||||||
|
method: 'POST',
|
||||||
|
body: { email, role },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchOrganizations() {
|
export async function fetchOrganizations() {
|
||||||
const { organizations } = await apiClient<{ organizations: AsDto<Organization>[] }>({
|
const { organizations } = await apiClient<{ organizations: AsDto<Organization>[] }>({
|
||||||
path: '/api/organizations',
|
path: '/api/organizations',
|
||||||
@@ -55,3 +63,44 @@ export async function deleteOrganization({ organizationId }: { organizationId: s
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchOrganizationMembers({ organizationId }: { organizationId: string }) {
|
||||||
|
const { members } = await apiClient<{ members: AsDto<OrganizationMember>[] }>({
|
||||||
|
path: `/api/organizations/${organizationId}/members`,
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
members: members.map(({ user, ...rest }) => coerceDates({ user: coerceDates(user), ...rest })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeOrganizationMember({ organizationId, memberId }: { organizationId: string; memberId: string }) {
|
||||||
|
await apiClient({
|
||||||
|
path: `/api/organizations/${organizationId}/members/${memberId}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMembership({ organizationId }: { organizationId: string }) {
|
||||||
|
const { member } = await apiClient<{ member: AsDto<OrganizationMember> }>({
|
||||||
|
path: `/api/organizations/${organizationId}/members/me`,
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
member: coerceDates(member),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOrganizationMemberRole({ organizationId, memberId, role }: { organizationId: string; memberId: string; role: OrganizationMemberRole }) {
|
||||||
|
const { member } = await apiClient<{ member: AsDto<OrganizationMember> }>({
|
||||||
|
path: `/api/organizations/${organizationId}/members/${memberId}`,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { role },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
member: coerceDates(member),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
|
import type { User } from 'better-auth/types';
|
||||||
|
|
||||||
export type Organization = {
|
export type Organization = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OrganizationMember = {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
user: User;
|
||||||
|
role: OrganizationMemberRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrganizationMemberRole = 'owner' | 'admin' | 'member';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { useCurrentUser } from '@/modules/users/composables/useCurrentUser';
|
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
import { createQuery } from '@tanstack/solid-query';
|
import { createQuery } from '@tanstack/solid-query';
|
||||||
import { createEffect, on } from 'solid-js';
|
import { createEffect, on } from 'solid-js';
|
||||||
|
import { useCurrentUser } from '@/modules/users/composables/useCurrentUser';
|
||||||
import { CreateOrganizationForm } from '../components/create-organization-form.component';
|
import { CreateOrganizationForm } from '../components/create-organization-form.component';
|
||||||
import { useCreateOrganization } from '../organizations.composables';
|
import { useCreateOrganization } from '../organizations.composables';
|
||||||
import { fetchOrganizations } from '../organizations.services';
|
import { fetchOrganizations } from '../organizations.services';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { CreateOrganizationForm } from '../components/create-organization-form.component';
|
import { CreateOrganizationForm } from '../components/create-organization-form.component';
|
||||||
import { useCreateOrganization } from '../organizations.composables';
|
import { useCreateOrganization } from '../organizations.composables';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { setValue } from '@modular-forms/solid';
|
||||||
|
import { useNavigate, useParams } from '@solidjs/router';
|
||||||
|
import { useMutation } from '@tanstack/solid-query';
|
||||||
|
import { onMount, Show } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
|
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||||
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
|
import { Button } from '@/modules/ui/components/button';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/modules/ui/components/select';
|
||||||
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
|
import {
|
||||||
|
TextField,
|
||||||
|
TextFieldLabel,
|
||||||
|
TextFieldRoot,
|
||||||
|
} from '@/modules/ui/components/textfield';
|
||||||
|
import { useCurrentUserRole } from '../organizations.composables';
|
||||||
|
import { ORGANIZATION_ROLES } from '../organizations.constants';
|
||||||
|
import { inviteOrganizationMember } from '../organizations.services';
|
||||||
|
|
||||||
|
type InvitableRole = 'member' | 'admin';
|
||||||
|
|
||||||
|
export const InviteMemberPage: Component = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const params = useParams();
|
||||||
|
const { getErrorMessage } = useI18nApiErrors({ t });
|
||||||
|
const { getIsAtLeastAdmin } = useCurrentUserRole({ organizationId: params.organizationId });
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!getIsAtLeastAdmin()) {
|
||||||
|
navigate(`/organizations/${params.organizationId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tRole = (role: InvitableRole) => t(`organizations.members.roles.${role}`);
|
||||||
|
|
||||||
|
const inviteMemberMutation = useMutation(() => ({
|
||||||
|
mutationFn: ({ email, role }: { email: string; role: InvitableRole }) =>
|
||||||
|
inviteOrganizationMember({
|
||||||
|
organizationId: params.organizationId,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['organizations', params.organizationId, 'invitations'],
|
||||||
|
});
|
||||||
|
createToast({
|
||||||
|
message: t('organizations.invite-member.success.message'),
|
||||||
|
description: t('organizations.invite-member.success.description'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
createToast({
|
||||||
|
message: t('organizations.invite-member.error.message'),
|
||||||
|
description: getErrorMessage({ error }),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { Form, Field, form } = createForm({
|
||||||
|
schema: v.object({
|
||||||
|
email: v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.trim(),
|
||||||
|
v.email(t('organizations.invite-member.form.email.required')),
|
||||||
|
v.toLowerCase(),
|
||||||
|
),
|
||||||
|
role: v.picklist([ORGANIZATION_ROLES.MEMBER, ORGANIZATION_ROLES.ADMIN]),
|
||||||
|
}),
|
||||||
|
initialValues: {
|
||||||
|
role: ORGANIZATION_ROLES.MEMBER,
|
||||||
|
},
|
||||||
|
onSubmit: async ({ email, role }) => {
|
||||||
|
inviteMemberMutation.mutate({ email, role });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="p-6 max-w-screen-md mx-auto mt-4">
|
||||||
|
<div class="border-b mb-6 pb-4">
|
||||||
|
<h1 class="text-xl font-bold">
|
||||||
|
{t('organizations.invite-member.title')}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{t('organizations.invite-member.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 max-w-xs mx-auto">
|
||||||
|
<Form>
|
||||||
|
<Field name="email">
|
||||||
|
{(field, inputProps) => (
|
||||||
|
<TextFieldRoot class="flex flex-col mb-6">
|
||||||
|
<TextFieldLabel for="email">
|
||||||
|
{t('organizations.invite-member.form.email.label')}
|
||||||
|
</TextFieldLabel>
|
||||||
|
<TextField
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
placeholder={t(
|
||||||
|
'organizations.invite-member.form.email.placeholder',
|
||||||
|
)}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
{field.error && (
|
||||||
|
<div class="text-red-500 text-sm">{field.error}</div>
|
||||||
|
)}
|
||||||
|
</TextFieldRoot>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field name="role">
|
||||||
|
{field => (
|
||||||
|
<div>
|
||||||
|
<label for="role" class="text-sm font-medium mb-1 block">
|
||||||
|
{t('organizations.invite-member.form.role.label')}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
id="role"
|
||||||
|
options={[
|
||||||
|
ORGANIZATION_ROLES.MEMBER,
|
||||||
|
ORGANIZATION_ROLES.ADMIN,
|
||||||
|
]}
|
||||||
|
itemComponent={props => (
|
||||||
|
<SelectItem item={props.item}>
|
||||||
|
{tRole(props.item.rawValue)}
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
value={field.value}
|
||||||
|
onChange={value =>
|
||||||
|
setValue(form, 'role', value as InvitableRole)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue<string>>
|
||||||
|
{state =>
|
||||||
|
tRole(state.selectedOption() as InvitableRole)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent />
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Button type="submit" class="w-full mt-6">
|
||||||
|
{t('organizations.invite-member.form.submit')}
|
||||||
|
<div class="i-tabler-send size-4 ml-1" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Show when={inviteMemberMutation.isError}>
|
||||||
|
<div class="text-red-500 text-sm">
|
||||||
|
{getErrorMessage({ error: inviteMemberMutation.error })}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import type { OrganizationMemberRole } from '../organizations.types';
|
||||||
|
import { A, useParams } from '@solidjs/router';
|
||||||
|
import { createMutation, createQuery } from '@tanstack/solid-query';
|
||||||
|
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
|
||||||
|
import { For, Show } from 'solid-js';
|
||||||
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
|
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||||
|
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||||
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
|
import { Button } from '@/modules/ui/components/button';
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuGroupLabel, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/modules/ui/components/dropdown-menu';
|
||||||
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/modules/ui/components/table';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/modules/ui/components/tooltip';
|
||||||
|
import { useCurrentUserRole } from '../organizations.composables';
|
||||||
|
import { ORGANIZATION_ROLES } from '../organizations.constants';
|
||||||
|
import { getIsMemberRoleDisabled } from '../organizations.models';
|
||||||
|
import { fetchOrganizationMembers, removeOrganizationMember, updateOrganizationMemberRole } from '../organizations.services';
|
||||||
|
|
||||||
|
const MemberList: Component = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { confirm } = useConfirmModal();
|
||||||
|
const query = createQuery(() => ({
|
||||||
|
queryKey: ['organizations', params.organizationId, 'members'],
|
||||||
|
queryFn: () => fetchOrganizationMembers({ organizationId: params.organizationId }),
|
||||||
|
}));
|
||||||
|
const { getErrorMessage } = useI18nApiErrors({ t });
|
||||||
|
|
||||||
|
const { getIsAtLeastAdmin, getRole } = useCurrentUserRole({ organizationId: params.organizationId });
|
||||||
|
|
||||||
|
const removeMemberMutation = createMutation(() => ({
|
||||||
|
mutationFn: ({ memberId }: { memberId: string }) => removeOrganizationMember({ organizationId: params.organizationId, memberId }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['organizations', params.organizationId, 'members'] });
|
||||||
|
|
||||||
|
createToast({
|
||||||
|
message: t('organizations.members.delete.success'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateMemberRoleMutation = createMutation(() => ({
|
||||||
|
mutationFn: ({ memberId, role }: { memberId: string; role: OrganizationMemberRole }) => updateOrganizationMemberRole({ organizationId: params.organizationId, memberId, role }),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['organizations', params.organizationId, 'members'] });
|
||||||
|
|
||||||
|
createToast({
|
||||||
|
message: t('organizations.members.update-role.success'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
createToast({
|
||||||
|
message: getErrorMessage({ error }),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleDelete = async ({ memberId }: { memberId: string }) => {
|
||||||
|
const confirmed = await confirm({
|
||||||
|
title: t('organizations.members.delete.confirm.title'),
|
||||||
|
message: t('organizations.members.delete.confirm.message'),
|
||||||
|
confirmButton: {
|
||||||
|
text: t('organizations.members.delete.confirm.confirm-button'),
|
||||||
|
variant: 'destructive',
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
text: t('organizations.members.delete.confirm.cancel-button'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMemberMutation.mutate({ memberId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateMemberRole = async ({ memberId, role }: { memberId: string; role: OrganizationMemberRole }) => {
|
||||||
|
await updateMemberRoleMutation.mutateAsync({ memberId, role });
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = createSolidTable({
|
||||||
|
get data() {
|
||||||
|
return query.data?.members ?? [];
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ header: 'Name', accessorKey: 'user.name' },
|
||||||
|
{ header: 'Email', accessorKey: 'user.email' },
|
||||||
|
{ header: 'Role', accessorKey: 'role', cell: data => t(`organizations.members.roles.${data.getValue<OrganizationMemberRole>()}`) },
|
||||||
|
{ header: 'Actions', id: 'actions', cell: data => (
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as={Button} variant="ghost" size="icon">
|
||||||
|
<div class="i-tabler-dots-vertical size-4" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDelete({ memberId: data.row.original.id })}
|
||||||
|
disabled={data.row.original.role === ORGANIZATION_ROLES.OWNER || !getIsAtLeastAdmin()}
|
||||||
|
>
|
||||||
|
<div class="i-tabler-user-x size-4 mr-2" />
|
||||||
|
{t('organizations.members.remove-from-organization')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuGroupLabel class="font-normal">{t('organizations.members.role')}</DropdownMenuGroupLabel>
|
||||||
|
<DropdownMenuRadioGroup value={data.row.original.role} onChange={role => handleUpdateMemberRole({ memberId: data.row.original.id, role: role as OrganizationMemberRole })}>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
value={ORGANIZATION_ROLES.OWNER}
|
||||||
|
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.OWNER })}
|
||||||
|
>
|
||||||
|
{t(`organizations.members.roles.owner`)}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
value={ORGANIZATION_ROLES.ADMIN}
|
||||||
|
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.ADMIN })}
|
||||||
|
>
|
||||||
|
{t(`organizations.members.roles.admin`)}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
value={ORGANIZATION_ROLES.MEMBER}
|
||||||
|
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.MEMBER })}
|
||||||
|
>
|
||||||
|
{t(`organizations.members.roles.member`)}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
) },
|
||||||
|
],
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<For each={table.getHeaderGroups()}>
|
||||||
|
{headerGroup => (
|
||||||
|
<TableRow>
|
||||||
|
<For each={headerGroup.headers}>{header => <TableHead>{flexRender(header.column.columnDef.header, header.getContext())}</TableHead>}</For>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<For each={table.getRowModel().rows}>
|
||||||
|
{row => <TableRow>{row.getVisibleCells().map(cell => <TableCell>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>)}</TableRow>}
|
||||||
|
</For>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const MembersPage: Component = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const params = useParams();
|
||||||
|
const { getIsAtLeastAdmin } = useCurrentUserRole({ organizationId: params.organizationId });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="p-6 max-w-screen-md mx-auto mt-4">
|
||||||
|
<div class="border-b mb-6 pb-4 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-bold">
|
||||||
|
{t('organizations.members.title')}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{t('organizations.members.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={getIsAtLeastAdmin()}
|
||||||
|
fallback={(
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button disabled>
|
||||||
|
<div class="i-tabler-plus size-4 mr-2" />
|
||||||
|
{t('organizations.members.invite-member')}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t('organizations.members.invite-member-disabled-tooltip')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button as={A} href={`/organizations/${params.organizationId}/invite`}>
|
||||||
|
<div class="i-tabler-plus size-4 mr-2" />
|
||||||
|
{t('organizations.members.invite-member')}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MemberList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { formatBytes } from '@corentinth/chisels';
|
||||||
|
import { useParams } from '@solidjs/router';
|
||||||
|
import { createQueries, keepPreviousData } from '@tanstack/solid-query';
|
||||||
|
import { createSignal, Show, Suspense } from 'solid-js';
|
||||||
import { DocumentUploadArea } from '@/modules/documents/components/document-upload-area.component';
|
import { DocumentUploadArea } from '@/modules/documents/components/document-upload-area.component';
|
||||||
import { createdAtColumn, DocumentsPaginatedList, standardActionsColumn, tagsColumn } from '@/modules/documents/components/documents-list.component';
|
import { createdAtColumn, DocumentsPaginatedList, standardActionsColumn, tagsColumn } from '@/modules/documents/components/documents-list.component';
|
||||||
import { useUploadDocuments } from '@/modules/documents/documents.composables';
|
import { useUploadDocuments } from '@/modules/documents/documents.composables';
|
||||||
import { fetchOrganizationDocuments, getOrganizationDocumentsStats } from '@/modules/documents/documents.services';
|
import { fetchOrganizationDocuments, getOrganizationDocumentsStats } from '@/modules/documents/documents.services';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { formatBytes } from '@corentinth/chisels';
|
|
||||||
import { useParams } from '@solidjs/router';
|
|
||||||
import { createQueries, keepPreviousData } from '@tanstack/solid-query';
|
|
||||||
import { createSignal, Show, Suspense } from 'solid-js';
|
|
||||||
|
|
||||||
export const OrganizationPage: Component = () => {
|
export const OrganizationPage: Component = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { Organization } from '../organizations.types';
|
import type { Organization } from '../organizations.types';
|
||||||
|
import { safely } from '@corentinth/chisels';
|
||||||
|
import { useParams } from '@solidjs/router';
|
||||||
|
import { createQuery } from '@tanstack/solid-query';
|
||||||
|
import { createSignal, Show, Suspense } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { buildTimeConfig } from '@/modules/config/config';
|
import { buildTimeConfig } from '@/modules/config/config';
|
||||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
@@ -8,11 +13,6 @@ import { Button } from '@/modules/ui/components/button';
|
|||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/modules/ui/components/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/modules/ui/components/card';
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { safely } from '@corentinth/chisels';
|
|
||||||
import { useParams } from '@solidjs/router';
|
|
||||||
import { createQuery } from '@tanstack/solid-query';
|
|
||||||
import { createSignal, Show, Suspense } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { useDeleteOrganization, useUpdateOrganization } from '../organizations.composables';
|
import { useDeleteOrganization, useUpdateOrganization } from '../organizations.composables';
|
||||||
import { organizationNameSchema } from '../organizations.schemas';
|
import { organizationNameSchema } from '../organizations.schemas';
|
||||||
import { fetchOrganization } from '../organizations.services';
|
import { fetchOrganization } from '../organizations.services';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { HttpClientOptions, ResponseType } from './http-client';
|
import type { HttpClientOptions, ResponseType } from './http-client';
|
||||||
import { buildTimeConfig } from '@/modules/config/config';
|
|
||||||
import { safely } from '@corentinth/chisels';
|
import { safely } from '@corentinth/chisels';
|
||||||
|
import { buildTimeConfig } from '@/modules/config/config';
|
||||||
import { httpClient } from './http-client';
|
import { httpClient } from './http-client';
|
||||||
import { isHttpErrorWithStatusCode } from './http-errors';
|
import { isHttpErrorWithStatusCode } from './http-errors';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LocaleKeys } from '@/modules/i18n/locales.types';
|
import type { LocaleKeys } from '@/modules/i18n/locales.types';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
|
|
||||||
export function useI18nApiErrors({ t = useI18n().t }: { t?: ReturnType<typeof useI18n>['t'] } = {}) {
|
export function useI18nApiErrors({ t = useI18n().t }: { t?: ReturnType<typeof useI18n>['t'] } = {}) {
|
||||||
const getTranslationFromApiErrorCode = ({ code }: { code: string }) => {
|
const getTranslationFromApiErrorCode = ({ code }: { code: string }) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { FetchOptions, ResponseType } from 'ofetch';
|
import type { FetchOptions, ResponseType } from 'ofetch';
|
||||||
|
import { ofetch } from 'ofetch';
|
||||||
import { buildTimeConfig } from '@/modules/config/config';
|
import { buildTimeConfig } from '@/modules/config/config';
|
||||||
import { demoHttpClient } from '@/modules/demo/demo-http-client';
|
import { demoHttpClient } from '@/modules/demo/demo-http-client';
|
||||||
import { ofetch } from 'ofetch';
|
|
||||||
|
|
||||||
export { ResponseType };
|
export { ResponseType };
|
||||||
export type HttpClientOptions<R extends ResponseType = 'json'> = Omit<FetchOptions<R>, 'baseURL'> & { url: string; baseUrl?: string };
|
export type HttpClientOptions<R extends ResponseType = 'json'> = Omit<FetchOptions<R>, 'baseURL'> & { url: string; baseUrl?: string };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
|
import { Button } from '@/modules/ui/components/button';
|
||||||
|
|
||||||
export const NotFoundPage: Component = () => {
|
export const NotFoundPage: Component = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ComponentProps, ParentComponent } from 'solid-js';
|
import type { ComponentProps, ParentComponent } from 'solid-js';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
|
||||||
import { createSignal } from 'solid-js';
|
import { createSignal } from 'solid-js';
|
||||||
|
import { Button } from '@/modules/ui/components/button';
|
||||||
|
|
||||||
export function useCopy() {
|
export function useCopy() {
|
||||||
const [getIsJustCopied, setIsJustCopied] = createSignal(false);
|
const [getIsJustCopied, setIsJustCopied] = createSignal(false);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { buildTimeConfig } from '@/modules/config/config';
|
|
||||||
import { buildUrl } from '@corentinth/chisels';
|
import { buildUrl } from '@corentinth/chisels';
|
||||||
|
import { buildTimeConfig } from '@/modules/config/config';
|
||||||
|
|
||||||
export function createVitrineUrl({ path, baseUrl = buildTimeConfig.vitrineBaseUrl }: { path: string; baseUrl?: string }): string {
|
export function createVitrineUrl({ path, baseUrl = buildTimeConfig.vitrineBaseUrl }: { path: string; baseUrl?: string }): string {
|
||||||
return buildUrl({ path, baseUrl });
|
return buildUrl({ path, baseUrl });
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { TaggingRule, TaggingRuleForCreation } from '../tagging-rules.types';
|
import type { TaggingRule, TaggingRuleForCreation } from '../tagging-rules.types';
|
||||||
|
import { insert, remove, setValue } from '@modular-forms/solid';
|
||||||
|
import { A } from '@solidjs/router';
|
||||||
|
import { For, Show } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||||
import { createForm } from '@/modules/shared/form/form';
|
import { createForm } from '@/modules/shared/form/form';
|
||||||
@@ -10,10 +14,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Separator } from '@/modules/ui/components/separator';
|
import { Separator } from '@/modules/ui/components/separator';
|
||||||
import { TextArea } from '@/modules/ui/components/textarea';
|
import { TextArea } from '@/modules/ui/components/textarea';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { insert, remove, setValue } from '@modular-forms/solid';
|
|
||||||
import { A } from '@solidjs/router';
|
|
||||||
import { For, Show } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { TAGGING_RULE_FIELDS, TAGGING_RULE_FIELDS_LOCALIZATION_KEYS, TAGGING_RULE_OPERATORS, TAGGING_RULE_OPERATORS_LOCALIZATION_KEYS } from '../tagging-rules.constants';
|
import { TAGGING_RULE_FIELDS, TAGGING_RULE_FIELDS_LOCALIZATION_KEYS, TAGGING_RULE_OPERATORS, TAGGING_RULE_OPERATORS_LOCALIZATION_KEYS } from '../tagging-rules.constants';
|
||||||
|
|
||||||
export const TaggingRuleForm: Component<{
|
export const TaggingRuleForm: Component<{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { TaggingRuleForCreation } from '../tagging-rules.types';
|
import type { TaggingRuleForCreation } from '../tagging-rules.types';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
|
||||||
import { useNavigate, useParams } from '@solidjs/router';
|
import { useNavigate, useParams } from '@solidjs/router';
|
||||||
import { createMutation } from '@tanstack/solid-query';
|
import { createMutation } from '@tanstack/solid-query';
|
||||||
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { TaggingRuleForm } from '../components/tagging-rule-form.component';
|
import { TaggingRuleForm } from '../components/tagging-rule-form.component';
|
||||||
import { createTaggingRule } from '../tagging-rules.services';
|
import { createTaggingRule } from '../tagging-rules.services';
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { TaggingRule } from '../tagging-rules.types';
|
import type { TaggingRule } from '../tagging-rules.types';
|
||||||
|
import { A, useParams } from '@solidjs/router';
|
||||||
|
import { createMutation, createQuery } from '@tanstack/solid-query';
|
||||||
|
import { For, Match, Show, Switch } from 'solid-js';
|
||||||
import { useConfig } from '@/modules/config/config.provider';
|
import { useConfig } from '@/modules/config/config.provider';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { queryClient } from '@/modules/shared/query/query-client';
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
import { Alert } from '@/modules/ui/components/alert';
|
import { Alert } from '@/modules/ui/components/alert';
|
||||||
import { Button } from '@/modules/ui/components/button';
|
import { Button } from '@/modules/ui/components/button';
|
||||||
import { EmptyState } from '@/modules/ui/components/empty';
|
import { EmptyState } from '@/modules/ui/components/empty';
|
||||||
import { A, useParams } from '@solidjs/router';
|
|
||||||
import { createMutation, createQuery } from '@tanstack/solid-query';
|
|
||||||
import { For, Match, Show, Switch } from 'solid-js';
|
|
||||||
import { deleteTaggingRule, fetchTaggingRules } from '../tagging-rules.services';
|
import { deleteTaggingRule, fetchTaggingRules } from '../tagging-rules.services';
|
||||||
|
|
||||||
const TaggingRuleCard: Component<{ taggingRule: TaggingRule }> = (props) => {
|
const TaggingRuleCard: Component<{ taggingRule: TaggingRule }> = (props) => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { TaggingRuleForCreation } from '../tagging-rules.types';
|
import type { TaggingRuleForCreation } from '../tagging-rules.types';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
|
||||||
import { queryClient } from '@/modules/shared/query/query-client';
|
|
||||||
import { createToast } from '@/modules/ui/components/sonner';
|
|
||||||
import { useNavigate, useParams } from '@solidjs/router';
|
import { useNavigate, useParams } from '@solidjs/router';
|
||||||
import { createMutation, createQuery } from '@tanstack/solid-query';
|
import { createMutation, createQuery } from '@tanstack/solid-query';
|
||||||
import { Show } from 'solid-js';
|
import { Show } from 'solid-js';
|
||||||
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
|
import { queryClient } from '@/modules/shared/query/query-client';
|
||||||
|
import { createToast } from '@/modules/ui/components/sonner';
|
||||||
import { TaggingRuleForm } from '../components/tagging-rule-form.component';
|
import { TaggingRuleForm } from '../components/tagging-rule-form.component';
|
||||||
import { getTaggingRule, updateTaggingRule } from '../tagging-rules.services';
|
import { getTaggingRule, updateTaggingRule } from '../tagging-rules.services';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
import type { Tag } from '../tags.types';
|
import type { Tag } from '../tags.types';
|
||||||
import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxTrigger } from '@/modules/ui/components/combobox';
|
|
||||||
import { createQuery } from '@tanstack/solid-query';
|
import { createQuery } from '@tanstack/solid-query';
|
||||||
import { createSignal, For } from 'solid-js';
|
import { createSignal, For } from 'solid-js';
|
||||||
|
import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxTrigger } from '@/modules/ui/components/combobox';
|
||||||
import { fetchTags } from '../tags.services';
|
import { fetchTags } from '../tags.services';
|
||||||
import { Tag as TagComponent } from './tag.component';
|
import { Tag as TagComponent } from './tag.component';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Component, ComponentProps } from 'solid-js';
|
import type { Component, ComponentProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
type TagProps = {
|
type TagProps = {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { DialogTriggerProps } from '@kobalte/core/dialog';
|
import type { DialogTriggerProps } from '@kobalte/core/dialog';
|
||||||
import type { Component, JSX } from 'solid-js';
|
import type { Component, JSX } from 'solid-js';
|
||||||
import type { Tag as TagType } from '../tags.types';
|
import type { Tag as TagType } from '../tags.types';
|
||||||
|
import { getValues } from '@modular-forms/solid';
|
||||||
|
import { A, useParams } from '@solidjs/router';
|
||||||
|
import { createQuery } from '@tanstack/solid-query';
|
||||||
|
import { createSignal, For, Show, Suspense } from 'solid-js';
|
||||||
|
import * as v from 'valibot';
|
||||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||||
import { timeAgo } from '@/modules/shared/date/time-ago';
|
import { timeAgo } from '@/modules/shared/date/time-ago';
|
||||||
@@ -13,11 +18,6 @@ import { createToast } from '@/modules/ui/components/sonner';
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/modules/ui/components/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/modules/ui/components/table';
|
||||||
import { TextArea } from '@/modules/ui/components/textarea';
|
import { TextArea } from '@/modules/ui/components/textarea';
|
||||||
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
import { TextField, TextFieldLabel, TextFieldRoot } from '@/modules/ui/components/textfield';
|
||||||
import { getValues } from '@modular-forms/solid';
|
|
||||||
import { A, useParams } from '@solidjs/router';
|
|
||||||
import { createQuery } from '@tanstack/solid-query';
|
|
||||||
import { createSignal, For, Show, Suspense } from 'solid-js';
|
|
||||||
import * as v from 'valibot';
|
|
||||||
import { Tag } from '../components/tag.component';
|
import { Tag } from '../components/tag.component';
|
||||||
import { createTag, deleteTag, fetchTags, updateTag } from '../tags.services';
|
import { createTag, deleteTag, fetchTags, updateTag } from '../tags.services';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
import { createEffect } from 'solid-js';
|
||||||
import { useSession } from '@/modules/auth/auth.services';
|
import { useSession } from '@/modules/auth/auth.services';
|
||||||
import { buildTimeConfig } from '@/modules/config/config';
|
import { buildTimeConfig } from '@/modules/config/config';
|
||||||
import { createEffect } from 'solid-js';
|
|
||||||
import { trackingServices } from '../tracking.services';
|
import { trackingServices } from '../tracking.services';
|
||||||
|
|
||||||
export const IdentifyUser: Component = () => {
|
export const IdentifyUser: Component = () => {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { AlertRootProps } from '@kobalte/core/alert';
|
|||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import type { ComponentProps, ValidComponent } from 'solid-js';
|
import type { ComponentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Alert as AlertPrimitive } from '@kobalte/core/alert';
|
import { Alert as AlertPrimitive } from '@kobalte/core/alert';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const alertVariants = cva(
|
export const alertVariants = cva(
|
||||||
'relative w-full rounded-lg border px-4 py-3 text-sm [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:(absolute left-4 top-4 text-foreground)',
|
'relative w-full rounded-lg border px-4 py-3 text-sm [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:(absolute left-4 top-4 text-foreground)',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import type { ComponentProps } from 'solid-js';
|
import type { ComponentProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const badgeVariants = cva(
|
export const badgeVariants = cva(
|
||||||
'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:(outline-none ring-1.5 ring-ring)',
|
'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:(outline-none ring-1.5 ring-ring)',
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { ButtonRootProps } from '@kobalte/core/button';
|
|||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import type { JSX, ValidComponent } from 'solid-js';
|
import type { JSX, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Button as ButtonPrimitive } from '@kobalte/core/button';
|
import { Button as ButtonPrimitive } from '@kobalte/core/button';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
export const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) bg-inherit',
|
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) bg-inherit',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ComponentProps, ParentComponent } from 'solid-js';
|
import type { ComponentProps, ParentComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export function Card(props: ComponentProps<'div'>) {
|
export function Card(props: ComponentProps<'div'>) {
|
||||||
const [local, rest] = splitProps(props, ['class']);
|
const [local, rest] = splitProps(props, ['class']);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { CheckboxControlProps } from '@kobalte/core/checkbox';
|
import type { CheckboxControlProps } from '@kobalte/core/checkbox';
|
||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { JSX, ValidComponent, VoidProps } from 'solid-js';
|
import type { JSX, ValidComponent, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Checkbox as CheckboxPrimitive } from '@kobalte/core/checkbox';
|
import { Checkbox as CheckboxPrimitive } from '@kobalte/core/checkbox';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const CheckboxLabel = CheckboxPrimitive.Label;
|
export const CheckboxLabel = CheckboxPrimitive.Label;
|
||||||
export const Checkbox = CheckboxPrimitive;
|
export const Checkbox = CheckboxPrimitive;
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import type {
|
|||||||
} from '@kobalte/core/combobox';
|
} from '@kobalte/core/combobox';
|
||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { JSX, ParentProps, ValidComponent, VoidProps } from 'solid-js';
|
import type { JSX, ParentProps, ValidComponent, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Combobox as ComboboxPrimitive } from '@kobalte/core/combobox';
|
import { Combobox as ComboboxPrimitive } from '@kobalte/core/combobox';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const Combobox = ComboboxPrimitive;
|
export const Combobox = ComboboxPrimitive;
|
||||||
export const ComboboxDescription = ComboboxPrimitive.Description;
|
export const ComboboxDescription = ComboboxPrimitive.Description;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import type {
|
|||||||
CommandRootProps,
|
CommandRootProps,
|
||||||
} from 'cmdk-solid';
|
} from 'cmdk-solid';
|
||||||
import type { ComponentProps, VoidProps } from 'solid-js';
|
import type { ComponentProps, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Command as CommandPrimitive } from 'cmdk-solid';
|
import { Command as CommandPrimitive } from 'cmdk-solid';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
import { Dialog, DialogContent } from './dialog';
|
import { Dialog, DialogContent } from './dialog';
|
||||||
|
|
||||||
export function Command(props: CommandRootProps) {
|
export function Command(props: CommandRootProps) {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type {
|
|||||||
} from '@kobalte/core/dialog';
|
} from '@kobalte/core/dialog';
|
||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
|
import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Dialog as DialogPrimitive } from '@kobalte/core/dialog';
|
import { Dialog as DialogPrimitive } from '@kobalte/core/dialog';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const Dialog = DialogPrimitive;
|
export const Dialog = DialogPrimitive;
|
||||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import type {
|
|||||||
} from '@kobalte/core/dropdown-menu';
|
} from '@kobalte/core/dropdown-menu';
|
||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
|
import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from '@kobalte/core/dropdown-menu';
|
import { DropdownMenu as DropdownMenuPrimitive } from '@kobalte/core/dropdown-menu';
|
||||||
import { mergeProps, splitProps } from 'solid-js';
|
import { mergeProps, splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Component, ComponentProps, JSX } from 'solid-js';
|
import type { Component, ComponentProps, JSX } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const EmptyState: Component<{
|
export const EmptyState: Component<{
|
||||||
title: JSX.Element;
|
title: JSX.Element;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import type {
|
|||||||
} from '@kobalte/core/number-field';
|
} from '@kobalte/core/number-field';
|
||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { ComponentProps, ValidComponent, VoidProps } from 'solid-js';
|
import type { ComponentProps, ValidComponent, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { NumberField as NumberFieldPrimitive } from '@kobalte/core/number-field';
|
import { NumberField as NumberFieldPrimitive } from '@kobalte/core/number-field';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
import { textfieldLabel } from './textfield';
|
import { textfieldLabel } from './textfield';
|
||||||
|
|
||||||
export const NumberFieldHiddenInput = NumberFieldPrimitive.HiddenInput;
|
export const NumberFieldHiddenInput = NumberFieldPrimitive.HiddenInput;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import type {
|
|||||||
PopoverRootProps,
|
PopoverRootProps,
|
||||||
} from '@kobalte/core/popover';
|
} from '@kobalte/core/popover';
|
||||||
import type { ParentProps, ValidComponent } from 'solid-js';
|
import type { ParentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Popover as PopoverPrimitive } from '@kobalte/core/popover';
|
import { Popover as PopoverPrimitive } from '@kobalte/core/popover';
|
||||||
import { mergeProps, splitProps } from 'solid-js';
|
import { mergeProps, splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
export const PopoverTitle = PopoverPrimitive.Title;
|
export const PopoverTitle = PopoverPrimitive.Title;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { ProgressRootProps } from '@kobalte/core/progress';
|
import type { ProgressRootProps } from '@kobalte/core/progress';
|
||||||
import type { ParentProps, ValidComponent } from 'solid-js';
|
import type { ParentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Progress as ProgressPrimitive } from '@kobalte/core/progress';
|
import { Progress as ProgressPrimitive } from '@kobalte/core/progress';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const ProgressLabel = ProgressPrimitive.Label;
|
export const ProgressLabel = ProgressPrimitive.Label;
|
||||||
export const ProgressValueLabel = ProgressPrimitive.ValueLabel;
|
export const ProgressValueLabel = ProgressPrimitive.ValueLabel;
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type {
|
|||||||
SelectTriggerProps,
|
SelectTriggerProps,
|
||||||
} from '@kobalte/core/select';
|
} from '@kobalte/core/select';
|
||||||
import type { ParentProps, ValidComponent } from 'solid-js';
|
import type { ParentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Select as SelectPrimitive } from '@kobalte/core/select';
|
import { Select as SelectPrimitive } from '@kobalte/core/select';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const Select = SelectPrimitive;
|
export const Select = SelectPrimitive;
|
||||||
export const SelectValue = SelectPrimitive.Value;
|
export const SelectValue = SelectPrimitive.Value;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { SeparatorRootProps } from '@kobalte/core/separator';
|
import type { SeparatorRootProps } from '@kobalte/core/separator';
|
||||||
import type { ValidComponent } from 'solid-js';
|
import type { ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Separator as SeparatorPrimitive } from '@kobalte/core/separator';
|
import { Separator as SeparatorPrimitive } from '@kobalte/core/separator';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
type separatorProps<T extends ValidComponent = 'hr'> = SeparatorRootProps<T> & {
|
type separatorProps<T extends ValidComponent = 'hr'> = SeparatorRootProps<T> & {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import type {
|
|||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
|
import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Dialog as DialogPrimitive } from '@kobalte/core/dialog';
|
import { Dialog as DialogPrimitive } from '@kobalte/core/dialog';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { mergeProps, splitProps } from 'solid-js';
|
import { mergeProps, splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const Sheet = DialogPrimitive;
|
export const Sheet = DialogPrimitive;
|
||||||
export const SheetTrigger = DialogPrimitive.Trigger;
|
export const SheetTrigger = DialogPrimitive.Trigger;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ComponentProps } from 'solid-js';
|
import type { ComponentProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export function Skeleton(props: ComponentProps<'div'>) {
|
export function Skeleton(props: ComponentProps<'div'>) {
|
||||||
const [local, rest] = splitProps(props, ['class']);
|
const [local, rest] = splitProps(props, ['class']);
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import type {
|
|||||||
SwitchThumbProps,
|
SwitchThumbProps,
|
||||||
} from '@kobalte/core/switch';
|
} from '@kobalte/core/switch';
|
||||||
import type { ParentProps, ValidComponent, VoidProps } from 'solid-js';
|
import type { ParentProps, ValidComponent, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Switch as SwitchPrimitive } from '@kobalte/core/switch';
|
import { Switch as SwitchPrimitive } from '@kobalte/core/switch';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const SwitchLabel = SwitchPrimitive.Label;
|
export const SwitchLabel = SwitchPrimitive.Label;
|
||||||
export const Switch = SwitchPrimitive;
|
export const Switch = SwitchPrimitive;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ComponentProps } from 'solid-js';
|
import type { ComponentProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export function Table(props: ComponentProps<'table'>) {
|
export function Table(props: ComponentProps<'table'>) {
|
||||||
const [local, rest] = splitProps(props, ['class']);
|
const [local, rest] = splitProps(props, ['class']);
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import type {
|
|||||||
} from '@kobalte/core/tabs';
|
} from '@kobalte/core/tabs';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import type { ValidComponent, VoidProps } from 'solid-js';
|
import type { ValidComponent, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { Tabs as TabsPrimitive } from '@kobalte/core/tabs';
|
import { Tabs as TabsPrimitive } from '@kobalte/core/tabs';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
type tabsProps<T extends ValidComponent = 'div'> = TabsRootProps<T> & {
|
type tabsProps<T extends ValidComponent = 'div'> = TabsRootProps<T> & {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
||||||
import type { TextFieldTextAreaProps } from '@kobalte/core/text-field';
|
import type { TextFieldTextAreaProps } from '@kobalte/core/text-field';
|
||||||
import type { ValidComponent, VoidProps } from 'solid-js';
|
import type { ValidComponent, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { TextArea as TextFieldPrimitive } from '@kobalte/core/text-field';
|
import { TextArea as TextFieldPrimitive } from '@kobalte/core/text-field';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
type textAreaProps<T extends ValidComponent = 'textarea'> = VoidProps<
|
type textAreaProps<T extends ValidComponent = 'textarea'> = VoidProps<
|
||||||
TextFieldTextAreaProps<T> & {
|
TextFieldTextAreaProps<T> & {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import type {
|
|||||||
TextFieldRootProps,
|
TextFieldRootProps,
|
||||||
} from '@kobalte/core/text-field';
|
} from '@kobalte/core/text-field';
|
||||||
import type { ValidComponent, VoidProps } from 'solid-js';
|
import type { ValidComponent, VoidProps } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { TextField as TextFieldPrimitive } from '@kobalte/core/text-field';
|
import { TextField as TextFieldPrimitive } from '@kobalte/core/text-field';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
type textFieldProps<T extends ValidComponent = 'div'> =
|
type textFieldProps<T extends ValidComponent = 'div'> =
|
||||||
TextFieldRootProps<T> & {
|
TextFieldRootProps<T> & {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type {
|
|||||||
} from '@kobalte/core/toggle-group';
|
} from '@kobalte/core/toggle-group';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import type { Accessor, ParentProps, ValidComponent } from 'solid-js';
|
import type { Accessor, ParentProps, ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { ToggleGroup as ToggleGroupPrimitive } from '@kobalte/core/toggle-group';
|
import { ToggleGroup as ToggleGroupPrimitive } from '@kobalte/core/toggle-group';
|
||||||
import { createContext, createMemo, splitProps, useContext } from 'solid-js';
|
import { createContext, createMemo, splitProps, useContext } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
import { toggleVariants } from './toggle';
|
import { toggleVariants } from './toggle';
|
||||||
|
|
||||||
const ToggleGroupContext = createContext<Accessor<VariantProps<typeof toggleVariants>>>();
|
const ToggleGroupContext = createContext<Accessor<VariantProps<typeof toggleVariants>>>();
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { PolymorphicProps } from '@kobalte/core/polymorphic';
|
|||||||
import type { ToggleButtonRootProps } from '@kobalte/core/toggle-button';
|
import type { ToggleButtonRootProps } from '@kobalte/core/toggle-button';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import type { ValidComponent } from 'solid-js';
|
import type { ValidComponent } from 'solid-js';
|
||||||
import { cn } from '@/modules/shared/style/cn';
|
|
||||||
import { ToggleButton as ToggleButtonPrimitive } from '@kobalte/core/toggle-button';
|
import { ToggleButton as ToggleButtonPrimitive } from '@kobalte/core/toggle-button';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { splitProps } from 'solid-js';
|
import { splitProps } from 'solid-js';
|
||||||
|
import { cn } from '@/modules/shared/style/cn';
|
||||||
|
|
||||||
export const toggleVariants = cva(
|
export const toggleVariants = cva(
|
||||||
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors transition-property-[box-shadow,color,background-color] hover:(bg-muted text-muted-foreground) focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) data-[pressed]:(bg-accent text-accent-foreground) transition-shadow',
|
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors transition-property-[box-shadow,color,background-color] hover:(bg-muted text-muted-foreground) focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) data-[pressed]:(bg-accent text-accent-foreground) transition-shadow',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user