Compare commits
@@ -241,6 +241,15 @@
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ehowe",
|
||||
"name": "Eugene Howe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/756984?v=4",
|
||||
"profile": "https://github.com/ehowe",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ PUBLIC_URL=https://localhost:3001
|
||||
GENERATE_SOURCEMAP=false
|
||||
# Set it to the URL from where APIs will be accessible, for local development it should be localhost:3000/api/app (use your local port number instead)
|
||||
# REACT_APP_SERVERURL=http://localhost:8080/app
|
||||
# A 12 character long random app identifier. The value of this should be same as APP_ID which is a variable used by backend API.
|
||||
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
|
||||
REACT_APP_APPID=opensign
|
||||
|
||||
|
||||
# Backend ExpressJS config ****************************************************************************************************************************************************************************************
|
||||
# A 12 character long random app identifier. The value of this should be same as REACT_APP_APPID which is a variable used by Frontend React App.
|
||||
APP_ID=opensign
|
||||
# Name of the app. It will be visible in the verification emails sent out.
|
||||
appName=open_sign_server
|
||||
# A 12 character long random secret key that allows access to all the data. It is used in Parse dashboard config to view all the data in the database.
|
||||
@@ -111,3 +109,6 @@ CRUxFgQUDYlgGVxSxuOknhQc256x3++7BDwwMTAhMAkGBSsOAwIaBQAEFFjASdYl
|
||||
|
||||
# Provide Pass pharse of above PFX or p12 document
|
||||
PASS_PHRASE=opensign
|
||||
|
||||
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
|
||||
APP_ID=opensign
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
name: ci
|
||||
|
||||
# 👇 add this block
|
||||
permissions:
|
||||
contents: read # allow checkout & metadata-action to read repo
|
||||
id-token: write # needed by docker/metadata-action v4
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'staging'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
@@ -14,34 +21,46 @@ jobs:
|
||||
dockerfile: apps/OpenSign/Dockerhubfile
|
||||
- image: opensign/opensignserver
|
||||
dockerfile: apps/OpenSignServer/Dockerhubfile
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
|
||||
# 👇 Enable emulation so we can build for arm64 on amd64 runners
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# 👇 Enable Buildx for multi-arch builds
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
-
|
||||
name: Debug - List files opensign
|
||||
|
||||
- name: Debug - List files opensign
|
||||
run: ls -R apps/OpenSign/ # Adjust the path as needed
|
||||
-
|
||||
name: Debug - List files opensignserver
|
||||
|
||||
- name: Debug - List files opensignserver
|
||||
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
|
||||
-
|
||||
name: Build and push
|
||||
|
||||
- name: Build and push (multi-arch)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
push: true
|
||||
# 👇 Build for both AMD64 & ARM64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{$HOST_URL} {
|
||||
reverse_proxy client:3000
|
||||
handle_path /app/* {
|
||||
handle_path /api/* {
|
||||
reverse_proxy server:8080
|
||||
rewrite * /app{uri}
|
||||
rewrite * {uri}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
|
||||
- **PDF Template Creation:** OpenSign™ allows you to create and store PDF document templates for repeated use thereby saving you a lot of time & collect e-signatures seamlessly.
|
||||
- **OpenSign™ Drive:** It is a centralised secure vault for your digital documents that makes storing, signing, organizing, sharing & archieving your docs a breeze.
|
||||
- **Audit Trails & completion certificate:** Being a security focused solution, OpenSign™ makes it a top priority to save detailed logs for tracking document activities along with time-stamps, IP addresses, email IDs & phone numbers. A completion certificate is generated as soon as document is completed which contains all the document related logs for added safety.
|
||||
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/opensign-api-v-1) to start integrating it in your existing applications.
|
||||
- **API Support:** OpenSign™ API allows seamless integration into existing systems and software. You can generate an API key from the app and refer the [official API docs](https://docs.opensignlabs.com/docs/API-docs/v1.1) to start integrating it in your existing applications.
|
||||
- **Integrations:** The open source document signing experience becomes even more seamless because of integrations with various Cloud storage systems, CRMs & enterprise platforms. We also have a Zapier integration that allows you to integrate it with virtually any application.
|
||||
<img alt="Login page" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/8016edb6-7d6e-4185-ab72-82d2c4a16032" height='200' >
|
||||
<img alt="Dash_board" src="https://github.com/OpenSignLabs/OpenSign/assets/171803164/6e7b96f7-3154-4c25-bc1e-caf33925cc47" height='200'>
|
||||
@@ -73,12 +73,28 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
|
||||
|
||||
---
|
||||
|
||||
### Installation
|
||||
### Deploy
|
||||
|
||||
Note: The default MongoDB instance used in deployment is not persistant and will be cleared on every restart. To retain your data, configure and supply your own MongoDB connection URL.
|
||||
|
||||
#### DigitalOcean
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/OpenSignLabs/Deploy-OpenSign-to-Digital-Ocean/tree/main&refcode=30db1c901ab0)
|
||||
|
||||
#### Docker
|
||||
The simplest way to install OpenSign on your own server is using official docker images by running the following command -
|
||||
|
||||
**Command for linux/MacOS**
|
||||
```
|
||||
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
|
||||
```
|
||||
**Command for Windows (Powershell)**
|
||||
```
|
||||
$env:HOST_URL="https://opensign.yourdomain.com"; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml -OutFile docker-compose.yml; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile -OutFile Caddyfile; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev -OutFile .env.local_dev; Rename-Item -Path .env.local_dev -NewName .env.prod; docker compose up --force-recreate
|
||||
```
|
||||
**Command for Windows (CMD/Terminal)**
|
||||
```
|
||||
set HOST_URL=https://opensign.yourdomain.com && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && rename .env.local_dev .env.prod && docker compose up --force-recreate
|
||||
```
|
||||
Make sure that you have `Docker` and `git` installed before you run this command -
|
||||
|
||||
Please refer to the [Installation Guide](https://docs.opensignlabs.com/docs/self-host/docker/run-locally/) for detailed instructions on how to install OpenSign on your system.
|
||||
@@ -149,6 +165,7 @@ We would like to thank all our contributors and users for their support and feed
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehowe"><img src="https://avatars.githubusercontent.com/u/756984?v=4?s=100" width="100px;" alt="Eugene Howe"/><br /><sub><b>Eugene Howe</b></sub></a><br /><a href="#code-ehowe" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Use an official Node runtime as the base image
|
||||
FROM node:18
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy package.json and package-lock.json first to leverage Docker cache
|
||||
COPY ./package*.json ./
|
||||
|
||||
# Install application dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the current directory contents into the container
|
||||
COPY ./ .
|
||||
COPY ./.husky .
|
||||
|
||||
# Make port 3000 available to the world outside this container
|
||||
EXPOSE 3000
|
||||
|
||||
# Define environment variables if needed
|
||||
# ENV NODE_ENV production
|
||||
|
||||
# Run the application
|
||||
ENTRYPOINT npm run start
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Use an official Node runtime as the base image
|
||||
FROM node:18
|
||||
FROM node:22.14.0
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /usr/src/app
|
||||
@@ -13,16 +13,24 @@ RUN npm install
|
||||
# Copy the current directory contents into the container
|
||||
COPY apps/OpenSign/ .
|
||||
COPY apps/OpenSign/.husky .
|
||||
COPY apps/OpenSign/entrypoint.sh .
|
||||
|
||||
# make the entrypoint.sh file executable
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
# Define environment variables if needed
|
||||
ENV NODE_ENV=production
|
||||
ENV REACT_APP_DEPLOYMENT=free_selfhost
|
||||
|
||||
ENV GENERATE_SOURCEMAP=false
|
||||
# build
|
||||
RUN npm run build
|
||||
|
||||
# Inject env.js loader into index.html
|
||||
RUN sed -i '/<head>/a\<script src="/env.js"></script>' build/index.html
|
||||
|
||||
# Make port 3000 available to the world outside this container
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
# Run the application
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Tailwind Dark Mode Usage Guide
|
||||
|
||||
## Overview
|
||||
This guide shows how to use the new Tailwind utilities for better dark mode visibility in OpenSign.
|
||||
|
||||
## Button Styling
|
||||
|
||||
### VS Code-style Disabled Buttons
|
||||
```jsx
|
||||
// Option 1: Direct VS Code styling
|
||||
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||
Disabled Button
|
||||
</button>
|
||||
|
||||
// Option 2: Themed disabled styling
|
||||
<button className="op-btn btn-themed-disabled">
|
||||
Themed Button
|
||||
</button>
|
||||
|
||||
// Option 3: Conditional styling
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Dynamic Button
|
||||
</button>
|
||||
```
|
||||
|
||||
## Icon Styling
|
||||
|
||||
### Theme-aware Icons
|
||||
```jsx
|
||||
// Better visibility in dark mode
|
||||
<i className="fa-light fa-folder icon-improved"></i>
|
||||
|
||||
// Muted but still visible
|
||||
<i className="fa-light fa-plus icon-muted"></i>
|
||||
|
||||
// Disabled state
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
```
|
||||
|
||||
### CSS Variable Approach
|
||||
```jsx
|
||||
// Using CSS variables
|
||||
<i className="fa-light fa-search icon-themed"></i>
|
||||
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||
|
||||
// Inline styles with CSS variables
|
||||
<i
|
||||
className="fa-light fa-plus"
|
||||
style={{ color: 'var(--icon-color)' }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Legacy JavaScript Function (Still Supported)
|
||||
```jsx
|
||||
// Existing approach - still works
|
||||
<i
|
||||
className="fa-light fa-plus"
|
||||
style={{ color: getThemeIconColor() }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Text Styling
|
||||
|
||||
### Improved Gray Text
|
||||
```jsx
|
||||
// These automatically improve in dark mode
|
||||
<span className="text-gray-500">More visible in dark mode</span>
|
||||
<span className="text-gray-400">Muted but readable</span>
|
||||
<span className="text-gray-600">Clear text</span>
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Toolbar with Better Visibility
|
||||
```jsx
|
||||
const Toolbar = () => (
|
||||
<div className="flex space-x-2 p-2">
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-plus icon-improved"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-edit icon-improved"></i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Form with Disabled States
|
||||
```jsx
|
||||
const Form = ({ isSubmitting }) => (
|
||||
<form>
|
||||
<input className="op-input" />
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${isSubmitting ? 'op-btn-vscode-disabled' : ''}`}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
```
|
||||
|
||||
## React-Tour and Tooltip Dark Mode Support
|
||||
|
||||
### React-Tour Modals
|
||||
The react-tour modals now automatically support dark mode with VS Code-inspired styling:
|
||||
|
||||
```jsx
|
||||
// These components automatically get dark mode styling
|
||||
<Tour
|
||||
onRequestClose={closeTour}
|
||||
steps={tourConfig}
|
||||
isOpen={isOpen}
|
||||
rounded={5}
|
||||
/>
|
||||
```
|
||||
|
||||
### ReactTooltip Components
|
||||
All ReactTooltip instances now support dark mode:
|
||||
|
||||
```jsx
|
||||
// Automatically styled for dark mode
|
||||
<ReactTooltip id="my-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px]">
|
||||
<p>Tooltip content</p>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
```
|
||||
|
||||
### HoverCard Balloon UI
|
||||
The balloon tooltips in OpenSign Drive now properly support dark mode:
|
||||
|
||||
```jsx
|
||||
// These automatically get dark styling in dark mode
|
||||
<HoverCard>
|
||||
<HoverCardContent>
|
||||
Document information
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
```
|
||||
|
||||
## Dark Mode Features Added
|
||||
|
||||
### 1. **React-Tour Modal Styling**
|
||||
- Background: `#1F2937` (VS Code modal background)
|
||||
- Text: `#E5E7EB` (soft white for readability)
|
||||
- Borders: `#374151` (subtle borders)
|
||||
- Buttons: VS Code-style primary/secondary buttons
|
||||
|
||||
### 2. **ReactTooltip Styling**
|
||||
- Background: `#1F2937` with proper contrast
|
||||
- Border: `#374151` for definition
|
||||
- Box shadow: Enhanced for dark backgrounds
|
||||
- Text: `#E5E7EB` for optimal readability
|
||||
|
||||
### 3. **HoverCard Balloon UI**
|
||||
- Background: `#1F2937` (matches VS Code)
|
||||
- Text: `#E5E7EB` for readability
|
||||
- Arrow: Automatically matches background color
|
||||
- Enhanced shadows for dark backgrounds
|
||||
|
||||
### 4. **React-Datepicker Support**
|
||||
- Calendar background: `#1F2937`
|
||||
- Selected dates: VS Code blue (`#007ACC`)
|
||||
- Hover states: Proper contrast ratios
|
||||
- Navigation arrows: Themed appropriately
|
||||
|
||||
## CSS Classes Reference
|
||||
|
||||
| Class | Purpose | Dark Mode Color |
|
||||
|-------|---------|----------------|
|
||||
| `icon-improved` | Better icon visibility | `#CCCCCC` |
|
||||
| `icon-muted` | Muted but visible icons | `#999999` |
|
||||
| `icon-disabled` | Disabled icon state | `#858585` |
|
||||
| `op-btn-vscode-disabled` | VS Code disabled button | Background: `#3C3C3C` |
|
||||
| `btn-themed-disabled` | Themed disabled button | Uses CSS variables |
|
||||
| `icon-themed` | Variable-based icon color | `var(--icon-color)` |
|
||||
| `.reactour__helper` | `#1F2937` background |
|
||||
| `.react-tooltip` | `#1F2937` background |
|
||||
| `.HoverCardContent` | `#1F2937` background |
|
||||
| `.react-datepicker` | `#1F2937` background |
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From JavaScript Function to Tailwind
|
||||
```jsx
|
||||
// Before
|
||||
<i style={{ color: getThemeIconColor() }} className="fa-light fa-plus" />
|
||||
|
||||
// After
|
||||
<i className="fa-light fa-plus icon-improved" />
|
||||
```
|
||||
|
||||
### From Hardcoded Colors to Theme-aware
|
||||
```jsx
|
||||
// Before
|
||||
<i className="fa-light fa-plus text-gray-500" />
|
||||
|
||||
// After (automatic improvement)
|
||||
<i className="fa-light fa-plus text-gray-500" />
|
||||
// OR explicitly
|
||||
<i className="fa-light fa-plus icon-improved" />
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
|
||||
All existing tooltip and tour components will automatically inherit the new dark mode styling when the theme is set to `opensigndark`. No code changes required for existing implementations.
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
ENV_FILE=./build/env.js
|
||||
DOTENV_FILE=./.env.prod # ✅ use .env.prod
|
||||
|
||||
echo "Generating runtime env file at $ENV_FILE..."
|
||||
|
||||
echo "window.RUNTIME_ENV = {" > $ENV_FILE
|
||||
|
||||
# List of keys to include
|
||||
RUNTIME_KEYS="REACT_APP_SERVERURL"
|
||||
|
||||
for key in $RUNTIME_KEYS; do
|
||||
# First check docker env (-e), fallback to .env file
|
||||
value=$(printenv "$key")
|
||||
|
||||
if [ -z "$value" ] && [ -f "$DOTENV_FILE" ]; then
|
||||
# fallback: read from .env
|
||||
value=$(grep "^$key=" "$DOTENV_FILE" | cut -d '=' -f2- | tr -d '\r\n' | sed 's/"/\\"/g')
|
||||
else
|
||||
value=$(echo "$value" | sed 's/"/\\"/g')
|
||||
fi
|
||||
|
||||
echo " $key: \"$value\"," >> $ENV_FILE
|
||||
done
|
||||
|
||||
echo "};" >> $ENV_FILE
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Tailwind Dark Mode Usage Examples for OpenSign
|
||||
*
|
||||
* This file demonstrates how to use the new Tailwind utilities
|
||||
* for better dark mode visibility of buttons and icons.
|
||||
*/
|
||||
|
||||
// Example 1: VS Code-style disabled buttons
|
||||
const DisabledButtonExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Option A: Using the VS Code disabled style */}
|
||||
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||
VS Code Style Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option B: Using themed disabled style */}
|
||||
<button className="op-btn btn-themed-disabled">
|
||||
Themed Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option C: Conditional styling */}
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${isDisabled ? "op-btn-vscode-disabled" : ""}`}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Conditional Button
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 2: Icon visibility improvements
|
||||
const IconExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Theme-aware icons with better visibility */}
|
||||
<i className="fa-light fa-folder icon-improved"></i>
|
||||
<i className="fa-light fa-plus icon-muted"></i>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
|
||||
{/* Using CSS variables */}
|
||||
<i className="fa-light fa-search icon-themed"></i>
|
||||
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||
|
||||
{/* Gray text that automatically improves in dark mode */}
|
||||
<span className="text-gray-500">
|
||||
This text is now more visible in dark mode
|
||||
</span>
|
||||
<span className="text-gray-400">Muted but still readable</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 3: Using CSS variables in inline styles
|
||||
const InlineStyleExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Using CSS variables directly */}
|
||||
<i className="fa-light fa-plus" style={{ color: "var(--icon-color)" }} />
|
||||
|
||||
{/* Using the existing JavaScript function */}
|
||||
<i className="fa-light fa-minus" style={{ color: getThemeIconColor() }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 4: Toolbar with improved icons
|
||||
const ToolbarExample = () => {
|
||||
return (
|
||||
<div className="flex space-x-2 p-2">
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-plus icon-improved"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-edit icon-improved"></i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
DisabledButtonExamples,
|
||||
IconExamples,
|
||||
InlineStyleExamples,
|
||||
ToolbarExample
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Tailwind Dark Mode Usage Examples for OpenSign
|
||||
*
|
||||
* This file demonstrates how to use the new Tailwind utilities
|
||||
* for better dark mode visibility of buttons and icons.
|
||||
*/
|
||||
|
||||
// Example 1: VS Code-style disabled buttons
|
||||
const DisabledButtonExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Option A: Using the VS Code disabled style */}
|
||||
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||
VS Code Style Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option B: Using themed disabled style */}
|
||||
<button className="op-btn btn-themed-disabled">
|
||||
Themed Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option C: Conditional styling */}
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Conditional Button
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 2: Icon visibility improvements
|
||||
const IconExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Theme-aware icons with better visibility */}
|
||||
<i className="fa-light fa-folder icon-improved"></i>
|
||||
<i className="fa-light fa-plus icon-muted"></i>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
|
||||
{/* Using CSS variables */}
|
||||
<i className="fa-light fa-search icon-themed"></i>
|
||||
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||
|
||||
{/* Gray text that automatically improves in dark mode */}
|
||||
<span className="text-gray-500">This text is now more visible in dark mode</span>
|
||||
<span className="text-gray-400">Muted but still readable</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 3: Using CSS variables in inline styles
|
||||
const InlineStyleExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Using CSS variables directly */}
|
||||
<i
|
||||
className="fa-light fa-plus"
|
||||
style={{ color: 'var(--icon-color)' }}
|
||||
/>
|
||||
|
||||
{/* Using the existing JavaScript function */}
|
||||
<i
|
||||
className="fa-light fa-minus"
|
||||
style={{ color: getThemeIconColor() }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 4: Toolbar with improved icons
|
||||
const ToolbarExample = () => {
|
||||
return (
|
||||
<div className="flex space-x-2 p-2">
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-plus icon-improved"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-edit icon-improved"></i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
DisabledButtonExamples,
|
||||
IconExamples,
|
||||
InlineStyleExamples,
|
||||
ToolbarExample
|
||||
};
|
||||
@@ -3,19 +3,20 @@
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
|
||||
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> -->
|
||||
<!-- <link rel="apple-touch-icon" href="/logo192.png" /> -->
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
||||
<title>...Loading</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" style="touch-action:pan-x pan-y;"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,74 +1,91 @@
|
||||
{
|
||||
"name": "open_sign",
|
||||
"version": "0.1.0",
|
||||
"version": "2.37.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@lottiefiles/dotlottie-react": "^0.13.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.18.3",
|
||||
"@mui/icons-material": "^5.15.10",
|
||||
"@mui/material": "^5.15.10",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.1.6",
|
||||
"@react-pdf/renderer": "^4.1.6",
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"axios": "^1.7.9",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
"@radix-ui/themes": "^3.3.0",
|
||||
"@reactour/tour": "^3.8.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@usewaypoint/block-avatar": "^0.0.3",
|
||||
"@usewaypoint/block-button": "^0.0.3",
|
||||
"@usewaypoint/block-columns-container": "^0.0.3",
|
||||
"@usewaypoint/block-container": "^0.0.2",
|
||||
"@usewaypoint/block-divider": "^0.0.4",
|
||||
"@usewaypoint/block-heading": "^0.0.3",
|
||||
"@usewaypoint/block-html": "^0.0.3",
|
||||
"@usewaypoint/block-image": "^0.0.5",
|
||||
"@usewaypoint/block-spacer": "^0.0.3",
|
||||
"@usewaypoint/block-text": "^0.0.7",
|
||||
"@usewaypoint/document-core": "^0.0.6",
|
||||
"@usewaypoint/email-builder": "^0.0.9",
|
||||
"axios": "^1.13.5",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"highlight.js": "^11.9.0",
|
||||
"i18next": "^25.8.13",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"juice": "^11.1.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"nth-check": "^2.1.1",
|
||||
"parse": "^5.3.0",
|
||||
"parse": "^8.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pkijs": "^3.3.3",
|
||||
"prettier": "^3.8.1",
|
||||
"print-js": "^1.6.0",
|
||||
"radix-ui": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-confetti": "^6.2.2",
|
||||
"react-cookie": "^7.2.2",
|
||||
"react-datepicker": "^7.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dnd-multi-backend": "^9.0.0",
|
||||
"react-dnd-touch-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-quill-new": "^3.3.3",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-konva": "^19.2.2",
|
||||
"react-pdf": "^10.4.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.4.14",
|
||||
"react-router": "^7.1.5",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.13.1",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-select": "^5.10.0",
|
||||
"react-signature-canvas": "^1.0.7",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-select": "^5.10.2",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
"react-timezone-select": "^3.2.8",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-web-share": "^2.0.2",
|
||||
"reactour": "^1.19.4",
|
||||
"react-to-print": "^3.3.0",
|
||||
"react-tooltip": "^5.30.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"regex-parser": "^2.3.0",
|
||||
"serve": "^14.2.4",
|
||||
"styled-components": "^5.3.0",
|
||||
"web-vitals": "^4.2.4",
|
||||
"ws": "^8.18.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
"regex-parser": "^2.3.1",
|
||||
"serve": "^14.2.5",
|
||||
"web-vitals": "^5.1.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run version && react-scripts build",
|
||||
"start-dev": "react-scripts start",
|
||||
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
"start-dev": "vite",
|
||||
"dev": "vite",
|
||||
"preview": "vite preview",
|
||||
"start": "serve -s build",
|
||||
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
|
||||
"version-win": "powershell -Command \"Invoke-RestMethod -Uri 'https://api.github.com/repos/opensignlabs/opensign/releases/latest' | Select-Object -ExpandProperty tag_name | Out-File -FilePath ./public/version.txt\"",
|
||||
"build-win": "npm run version-win && react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"build-win": "npm run version-win && vite build",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"release": "standard-version",
|
||||
"commit": "cz"
|
||||
},
|
||||
@@ -96,33 +113,41 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/runtime-corejs2": "^7.26.9",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-loader": "^9.2.1",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/runtime-corejs2": "^7.28.6",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"babel-loader": "^10.0.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"css-loader": "^7.1.2",
|
||||
"daisyui": "^4.12.23",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv-webpack": "^8.1.0",
|
||||
"eslint": "^9.20.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"lint-staged": "^15.4.3",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.5.0",
|
||||
"pretty-quick": "^4.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"concurrently": "^9.2.1",
|
||||
"css-loader": "^7.1.4",
|
||||
"daisyui": "^4.12.24",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^28.1.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"postcss": "^8.5.6",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"tmp": "^0.2.5",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"overrides": {
|
||||
"nth-check": "$nth-check",
|
||||
"ws": "$ws"
|
||||
"tmp": "$tmp",
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || 22"
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"header-news": "Neue Funktion: Benutzer des Teams-Plans können jetzt ihre eigenen AWS S3-Buckets für die Dateispeicherung integrieren",
|
||||
"header-news-btn": "Jetzt einrichten",
|
||||
"sandbox-news": "Dies ist eine Sandbox-Umgebung. Bitte nicht für produktive Zwecke verwenden.",
|
||||
"create-account": "Konto erstellen",
|
||||
"login": "Anmelden",
|
||||
"language": "Sprache",
|
||||
"dark-mode": "Dunkelmodus",
|
||||
"name": "Name",
|
||||
"phone": "Telefon",
|
||||
"phone-optional": "optional",
|
||||
"phone-optional": "Optional",
|
||||
"email": "E-Mail",
|
||||
"company": "Unternehmen",
|
||||
"job-title": "Berufsbezeichnung",
|
||||
@@ -24,6 +26,8 @@
|
||||
"Name": "Name",
|
||||
"Date": "Datum"
|
||||
},
|
||||
"folder": "Ordner",
|
||||
"pdf": "Pdf",
|
||||
"context-menu": {
|
||||
"Download": "Herunterladen",
|
||||
"Rename": "Umbenennen",
|
||||
@@ -39,8 +43,14 @@
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"upgrade-now": "Jetzt upgraden",
|
||||
"contact-now": "Jetzt kontaktieren",
|
||||
"upgrade-to": "Upgrade zu",
|
||||
"plan": "Plan",
|
||||
"connect": "Verbinden",
|
||||
"connect-to-g-drive": "Mit Google Drive verbinden",
|
||||
"reconnect-to-g-drive": "Erneut mit Google Drive verbinden",
|
||||
"gdrive-info-connect": "Wenn Google Drive verbunden ist, wird das abgeschlossene Dokument im Ordner {{appName}} auf Google Drive gespeichert.",
|
||||
"subscription-renew-warning": "Ihr Abonnement läuft in {{remainingDays}} Tagen ab. Bitte verlängern Sie Ihr Abonnement.",
|
||||
"subscribe-card-teamplan": "Entfesseln Sie die volle Kraft der Zusammenarbeit! Erstellen Sie unbegrenzt Organisationen, Teams und Hierarchien. Teilen Sie Vorlagen nahtlos zwischen Teams und weisen Sie benutzerdefinierte Benutzerrollen zu. Optimieren Sie Ihren Workflow noch heute!",
|
||||
"subscribe-card-plan": "Entsperren Sie Premium-Funktionen ab nur {{premiumPrice}}/Monat. Genießen Sie eine verbesserte Leistung und zahlen Sie nur {{addonPrice}} pro zusätzlichem Credit nach den enthaltenen Premium-Credits.",
|
||||
"user-name-limit-char": "Um einen Benutzernamen mit weniger als 8 Zeichen zu haben, abonnieren Sie bitte.",
|
||||
@@ -54,17 +64,18 @@
|
||||
"welcome": "Willkommen zurück!",
|
||||
"Login-to-your-account": "Melden Sie sich bei Ihrem Konto an",
|
||||
"password": "Passwort",
|
||||
"forgot-password": "Passwort vergessen?",
|
||||
"forgot-password": "Passwort vergessen",
|
||||
"loading": "Wird geladen...",
|
||||
"of": "von",
|
||||
"sign-SSO": "Mit SSO anmelden",
|
||||
"login-page": "Anmeldeseite",
|
||||
"or": "ODER",
|
||||
"signup-page": "Registrierungsseite",
|
||||
"password-match-length": "Neues Passwort und Bestätigungspasswort sollten übereinstimmen",
|
||||
"password-length": "Das Passwort sollte 8 Zeichen lang sein",
|
||||
"password-case": "Das Passwort sollte einen Großbuchstaben, einen Kleinbuchstaben und eine Zahl enthalten",
|
||||
"password-special-char": "Das Passwort sollte ein Sonderzeichen enthalten",
|
||||
"agreee": "Ich stimme den",
|
||||
"agree": "Ich stimme den",
|
||||
"term": "Nutzungsbedingungen zu",
|
||||
"subscribe-to-opensign": "Abonnieren Sie den OpenSign-Newsletter",
|
||||
"register": "Registrieren",
|
||||
@@ -79,6 +90,7 @@
|
||||
"Storage": "Speicher",
|
||||
"Signing certificate": "Signierzertifikat",
|
||||
"Teams": "Teams",
|
||||
"General": "Allgemein",
|
||||
"Teams-Children": {
|
||||
"Organizations": "Organisationen",
|
||||
"OrgAdmins": "OrgAdmins"
|
||||
@@ -109,7 +121,8 @@
|
||||
"Teams": "Teams",
|
||||
"Team-update-successfully": "Team erfolgreich aktualisiert.",
|
||||
"Users": "Benutzer"
|
||||
}
|
||||
},
|
||||
"Global SMTP": "Globales SMTP"
|
||||
},
|
||||
"dashboard-card": {
|
||||
"Need your Signature": "Benötigt Ihre Unterschrift",
|
||||
@@ -141,6 +154,7 @@
|
||||
"Quick send": "Schnell senden",
|
||||
"Edit": "Bearbeiten",
|
||||
"Share with team": "Mit Team teilen",
|
||||
"Share with user": "Mit Kollegen teilen",
|
||||
"Share": "Teilen",
|
||||
"View": "Ansehen",
|
||||
"option": "Option",
|
||||
@@ -149,7 +163,15 @@
|
||||
"Copy Public URL": "Öffentliche URL kopieren",
|
||||
"extend-expiry-date": "Ablaufdatum verlängern",
|
||||
"Duplicate Template": "Vorlage duplizieren",
|
||||
"Duplicate": "Duplikat"
|
||||
"Duplicate": "Duplikat",
|
||||
"daily-mail-quota": "Tägliches E-Mail-Kontingent",
|
||||
"Save as template": "Als Vorlage speichern",
|
||||
"Fix & resend": "Korrigieren und erneut senden",
|
||||
"Kiosk Mode": "Kiosk Modus",
|
||||
"Reset password": "Passwort zurücksetzen",
|
||||
"View formdata": "Formulardaten ansehen",
|
||||
"Download all formdata": "Alle Formulardaten herunterladen",
|
||||
"API credits": "API-Credits"
|
||||
},
|
||||
"report-heading": {
|
||||
"Sr.No": "Nr.",
|
||||
@@ -169,9 +191,23 @@
|
||||
"Name": "Name",
|
||||
"Status": "Status",
|
||||
"created-date": "Erstellungsdatum",
|
||||
"Type": "Typ",
|
||||
"Type": "Type",
|
||||
"Logs": "Protokolle",
|
||||
"Expiry-date": "Ablaufdatum"
|
||||
"Expiry-date": "Ablaufdatum",
|
||||
"Company": "Unternehmen",
|
||||
"JobTitle": "Berufsbezeichnung",
|
||||
"Time to complete (Days)": "Zeit zur Fertigstellung (Tage)",
|
||||
"Auto reminder": "Automatische Erinnerung",
|
||||
"Remind once in every (Days)": "Erinnere einmal alle (Tage)",
|
||||
"Enable OTP verification": "OTP-Verifizierung aktivieren",
|
||||
"Enable Tour": "Tour aktivieren",
|
||||
"Notify on signatures": "Bei Signaturen benachrichtigen",
|
||||
"Allow modifications": "Änderungen zulassen",
|
||||
"Redirect url": "Weiterleitungs-URL",
|
||||
"Created Date": "Erstellungsdatum",
|
||||
"Updated Date": "Aktualisierungsdatum",
|
||||
"Expiry Date": "Ablaufdatum",
|
||||
"Sent Date": "Sendedatum"
|
||||
},
|
||||
"report-help": {
|
||||
"Draft Documents": "Dies sind Dokumente, die Sie begonnen, aber noch nicht zum Versenden fertiggestellt haben.",
|
||||
@@ -183,16 +219,15 @@
|
||||
"Contactbook": "Dies ist eine Liste von Kontakten/Unterzeichnern, die Sie hinzugefügt haben. Diese erscheinen als Vorschläge, wenn Sie neue Unterzeichner hinzufügen möchten.",
|
||||
"Templates": "Dies ist eine Liste von Vorlagen, die Ihnen zur Verfügung stehen, um Dokumente zu erstellen. Sie können die Schaltfläche 'Verwenden' anklicken, um ein neues Dokument mit einer Vorlage zu erstellen, das Dokument zu ändern und Unterzeichner im nächsten Schritt hinzuzufügen."
|
||||
},
|
||||
"form-name": {
|
||||
"Sign Yourself": "Selbst unterschreiben",
|
||||
"Request Signatures": "Signaturen anfordern",
|
||||
"New Template": "Neue Vorlage"
|
||||
},
|
||||
"Sign Yourself": "Selbst unterschreiben",
|
||||
"Request Signatures": "Signaturen anfordern",
|
||||
"New Template": "Neue Vorlage",
|
||||
"file-type": "pdf, png, jpg, jpeg",
|
||||
"docx": "docx",
|
||||
"file-selected": "Datei ausgewählt",
|
||||
"files-selected": "Datei(en) ausgewählt",
|
||||
"template-title": "Vorlagentitel",
|
||||
"document-title": "Dokumenttitel",
|
||||
"title": "Titel",
|
||||
"description": "Beschreibung",
|
||||
"time-to-complete": "Bearbeitungszeit (Tage)",
|
||||
"send-in-order": "In der Reihenfolge senden",
|
||||
@@ -206,7 +241,6 @@
|
||||
"auto-reminder": "Automatische Erinnerung",
|
||||
"remind-once": "Einmal erinnern alle (Tage)",
|
||||
"next": "Weiter",
|
||||
"select-folder": "Ordner auswählen",
|
||||
"OpenSign-drive": "{{appName}} Drive",
|
||||
"drive-document-status": {
|
||||
"Completed": "Abgeschlossen",
|
||||
@@ -235,17 +269,18 @@
|
||||
"signature": "Unterschrift",
|
||||
"upload-image": "Bild hochladen",
|
||||
"clear": "Löschen",
|
||||
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen",
|
||||
"upload-signature/Image": "Bitte Unterschrift/Bild hochladen/Stempel",
|
||||
"initials": "Initialen",
|
||||
"API": "API",
|
||||
"api-token": "API-Token",
|
||||
"regenerate-token": "Live-Token neu generieren",
|
||||
"remove-background": "Hintergrund entfernen",
|
||||
"generate-token": "Live-Token generieren",
|
||||
"view-docs": "Dokumentation ansehen",
|
||||
"generate-token-alert": "Sind Sie sicher, dass Sie das Token neu generieren möchten? Das alte Token wird ablaufen.",
|
||||
"yes": "Ja",
|
||||
"copied": "Kopiert",
|
||||
"something-went-wrong-mssg": "Etwas ist schiefgelaufen. Bitte versuchen Sie es später erneut.",
|
||||
"something-went-wrong-mssg": "Etwas ist schiefgelaufen, Das Aktualisieren dieser Seite kann das Problem lösen.",
|
||||
"token-generated": "Token erfolgreich generiert.",
|
||||
"webhook": "Webhook",
|
||||
"update-webhook": "Webhook aktualisieren",
|
||||
@@ -260,7 +295,7 @@
|
||||
"edit-team": "Team bearbeiten",
|
||||
"name-of-team": "Name des Teams",
|
||||
"prev": "Zurück",
|
||||
"no-data-avaliable": "Keine Daten verfügbar",
|
||||
"no-data-available": "Keine Daten verfügbar",
|
||||
"add-team": "Team hinzufügen",
|
||||
"page-not-found": "Seite nicht gefunden",
|
||||
"users-from-teams": "Benutzer aus Teams",
|
||||
@@ -269,9 +304,9 @@
|
||||
"deactivate": "deaktivieren",
|
||||
"this-user": "diesen Benutzer",
|
||||
"delete-user": "Benutzer löschen",
|
||||
"delete": "löschen",
|
||||
"delete": "Löschen",
|
||||
"add-user": "Benutzer hinzufügen",
|
||||
"password-generateed": "Das Passwort wird nur einmal generiert; stellen Sie sicher, dass Sie es kopieren.",
|
||||
"password-generated": "Das Passwort wird nur einmal generiert; stellen Sie sicher, dass Sie es kopieren.",
|
||||
"Team status": "Teamstatus",
|
||||
"user-deactivated": "Benutzer deaktiviert.",
|
||||
"user-activated": "Benutzer aktiviert.",
|
||||
@@ -281,6 +316,7 @@
|
||||
"make-template-public": "Vorlage öffentlich machen",
|
||||
"make-template-private": "Vorlage privat machen",
|
||||
"make-template-public-alert": "Sind Sie sicher, dass Sie diese Vorlage öffentlich machen möchten?",
|
||||
"make-template-private-alert-non": "Sind Sie sicher, dass Sie diese Vorlage privat machen möchten?",
|
||||
"make-template-private-alert": "Sind Sie sicher, dass Sie diese Vorlage privat machen möchten? Dadurch wird sie aus Ihrem öffentlichen Profil entfernt.",
|
||||
"public-role": "Öffentliche Rolle",
|
||||
"public-url": "Öffentliches Profil",
|
||||
@@ -299,14 +335,14 @@
|
||||
"send": "Senden",
|
||||
"quick-send-alert-1": "Alle Rollen in diesem Dokument sind derzeit mit Kontakten verknüpft. Um Kopien dieser Vorlage schnell an mehrere Unterzeichner zu senden, stellen Sie sicher, dass mindestens eine Rolle keinem Kontakt zugeordnet ist.",
|
||||
"quick-send-alert-2": "Bitte stellen Sie sicher, dass für alle Empfänger mindestens ein Signatur-Widget hinzugefügt wurde.",
|
||||
"quick-send-alert-3": "Bitte fügen Sie mindestens eine Rolle zu dieser Vorlage hinzu, um Kopien davon schnell an mehrere Unterzeichner zu senden.",
|
||||
"quick-send-alert-4": "Das Limit für Schnellsendungen wurde erreicht.",
|
||||
"quick-send-alert-3": "Bitte fügen Sie diesem template mindestens eine Rolle hinzu.",
|
||||
"quick-send-alert-4": "{{flow}} unterstützt bis zu {{count}} Unterzeichner pro Batch. Teilen Sie Ihre Liste auf und versuchen Sie es erneut.",
|
||||
"copy-link": "Link kopieren",
|
||||
"copy": "Kopieren",
|
||||
"revoke-document": "Dokument widerrufen",
|
||||
"revoke-document-alert": "Sind Sie sicher, dass Sie dieses Dokument widerrufen möchten?",
|
||||
"resend-mail": "E-Mail erneut senden",
|
||||
"resend-mail-help": "Sie können folgende Variablen verwenden, die durch ihre tatsächlichen Werte ersetzt werden: {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}.",
|
||||
"resend-mail-help": "Sie können folgende Variablen verwenden, die durch ihre tatsächlichen Werte ersetzt werden: {{document_title}}, {{sender_name}}, {{sender_mail}}, {{sender_phone}}, {{receiver_name}}, {{receiver_email}}, {{receiver_phone}}, {{expiry_date}}, {{company_name}}, {{signing_url}}, {{note}}.",
|
||||
"subject": "Betreff",
|
||||
"body": "Inhalt",
|
||||
"add-contact": "Kontakt hinzufügen",
|
||||
@@ -336,7 +372,7 @@
|
||||
"verify-email-1": "E-Mail verifizieren",
|
||||
"resend": "Erneut senden",
|
||||
"contact-details": "Kontaktdetails",
|
||||
"verify-email": "Bitte verifizieren Sie Ihre E-Mail!",
|
||||
"verify-email": "Bitte bestätigen Sie Ihre E-Mail, um fortzufahren. Dies ist ein einmaliger Schritt, um die mit Ihrem OpenSign-Konto verknüpfte E-Mail zu bestätigen.",
|
||||
"send-otp": "OTP senden",
|
||||
"otp-placeholder": "Verifizierungscode aus der E-Mail eingeben",
|
||||
"loading-doc": "Dokument wird geladen...",
|
||||
@@ -350,11 +386,14 @@
|
||||
"date": "Datum",
|
||||
"text": "Text",
|
||||
"text input": "Texteingabe",
|
||||
"cells": "Zellen",
|
||||
"checkbox": "Checkbox",
|
||||
"dropdown": "Dropdown",
|
||||
"radio button": "Radiobutton",
|
||||
"image": "Bild",
|
||||
"email": "E-Mail"
|
||||
"email": "E-Mail",
|
||||
"number": "Zahl",
|
||||
"draw": "Zeichnen"
|
||||
},
|
||||
"fields": "Felder",
|
||||
"recipients": "Empfänger",
|
||||
@@ -366,6 +405,7 @@
|
||||
"certificate": "Zertifikat",
|
||||
"decline": "Ablehnen",
|
||||
"finish": "Fertigstellen",
|
||||
"done": "Fertig",
|
||||
"mail": "E-Mail",
|
||||
"sign-now": "Jetzt unterzeichnen",
|
||||
"successfully-signed": "Erfolgreich unterzeichnet!",
|
||||
@@ -375,6 +415,10 @@
|
||||
"Email-verified-alert-1": "E-Mail wurde verifiziert.",
|
||||
"Email-verified-alert-2": "E-Mail wurde bereits verifiziert.",
|
||||
"upload-stamp-image": "Stempelbild hochladen",
|
||||
"draw-signature": "Unterschrift zeichnen",
|
||||
"draw-initials": "Initialen zeichnen",
|
||||
"enter-text": "Text eingeben",
|
||||
"enter-widgettype": "{{widgetType}} eingeben",
|
||||
"draw": "Zeichnen",
|
||||
"type": "Schreiben",
|
||||
"color-type": {
|
||||
@@ -387,27 +431,44 @@
|
||||
"upload": "Hochladen",
|
||||
"initial-teb": "Initialen",
|
||||
"signature-tab": "Unterschrift",
|
||||
"initial-alert": "Meine Initiale nicht gefunden",
|
||||
"initial-alert": "Meine Initiale wurde nicht gefunden",
|
||||
"copy-title": "Widget kopieren zu",
|
||||
"contact-delete-alert": "Sind Sie sicher, dass Sie diesen Kontakt löschen möchten?",
|
||||
"reset-password-alert-1": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.",
|
||||
"reset-password-alert-2": "Bitte richten Sie den E-Mail-Adapter ein.",
|
||||
"reset-password-alert-3": "Setzen Sie Ihr Passwort zurück",
|
||||
"faild-animation": "Animation konnte nicht geladen werden",
|
||||
"failed-animation": "Animation konnte nicht geladen werden",
|
||||
"apply": "Anwenden",
|
||||
"select-columns": "Spalten auswählen",
|
||||
"copy-type": {
|
||||
"All pages": "Alle Seiten",
|
||||
"All pages but last": "Alle Seiten außer der letzten",
|
||||
"All pages but first": "Alle Seiten außer der ersten",
|
||||
"Next to current widget": "Neben dem aktuellen Widget"
|
||||
"Next to current widget": "Neben dem aktuellen Widget",
|
||||
"Page range": "Seitenbereich"
|
||||
},
|
||||
"options": "Optionen",
|
||||
"minimun-check": "Minimale Anzahl",
|
||||
"minimum-check": "Minimale Anzahl",
|
||||
"maximum-check": "Maximale Anzahl",
|
||||
"cell-count": "Zellzahl",
|
||||
"default-value": "Standardwert",
|
||||
"formula": "Formel",
|
||||
"formula-placeholder": "Erstellen Sie eine Formel mit Zahlen-Widgets",
|
||||
"formula-no-widgets": "Für diesen Unterzeichner sind noch keine weiteren Zahlen-Widgets vorhanden.",
|
||||
"supported-operators": "Unterstützte Operatoren:",
|
||||
"formula-helper": "Klicken Sie auf einen Zahlen-Widget-Chip, um ihn einzufügen (z. B. {{number-1}} + {{number-2}}).",
|
||||
"formula-error-letter": "Widget-Namen müssen in doppelte geschweifte Klammern gesetzt werden (z. B. {{number-1}}).",
|
||||
"formula-error-characters": "Außerhalb der Klammern sind nur Zahlen sowie +, -, *, /, (, ) erlaubt.",
|
||||
"formula-error-braces": "Jede öffnende {{ muss eine passende }} haben.",
|
||||
"formula-error-operators": "Verwenden Sie bei Operatoren oder Klammern mindestens zwei Werte.",
|
||||
"formula-error-leading-operator": "Formeln dürfen nicht mit Operatoren beginnen.",
|
||||
"decimal-places": "Dezimalstellen",
|
||||
"number-of-decimal-places": "Anzahl der Dezimalstellen",
|
||||
"decimal-places-helper": "Auf 0 setzen für ganze Zahlen",
|
||||
"select": "Auswählen",
|
||||
"read-only": "Nur lesen",
|
||||
"read-only": "Ist schreibgeschützt",
|
||||
"hide-labels": "Labels ausblenden",
|
||||
"layout": "Layout",
|
||||
"checkbox": "Checkbox",
|
||||
"alert": "Warnung",
|
||||
"zoom-in": "Vergrößern",
|
||||
@@ -450,20 +511,21 @@
|
||||
"add-recipients": "Empfänger hinzufügen",
|
||||
"loading-mssg": "Das könnte etwas Zeit in Anspruch nehmen",
|
||||
"send-mail": "E-Mail senden",
|
||||
"signature-field-widget": "Für jeden Benutzer muss mindestens ein Signaturfeld hinzugefügt werden. Sie haben keine Signaturfelder für {{signersName}} hinzugefügt",
|
||||
"signature-field-widget": "Jeder Benutzer muss mindestens ein Signatur-Widget haben. Sie haben kein Signatur-Widget für {{signersName}} hinzugefügt.",
|
||||
"placeholder-alert-1": "Bitte stellen Sie sicher, dass für alle Empfänger mindestens ein Signatur-Widget hinzugefügt wurde.",
|
||||
"placeholder-alert-2": "Bitte bestätigen Sie, dass Sie das Textfeld ausgefüllt haben.",
|
||||
"placeholder-alert-3": "Sind Sie sicher, dass Sie dieses Dokument zur Unterzeichnung senden möchten?",
|
||||
"placeholder-alert-4": "Sie haben erfolgreich E-Mails an alle Empfänger gesendet!",
|
||||
"placeholder-mail-alert": "Sie haben erfolgreich eine E-Mail an {{name}} gesendet. Die nachfolgenden Unterzeichner erhalten eine E-Mail, sobald {{name}} das Dokument unterzeichnet.",
|
||||
"placeholder-alert-5": "Möchten Sie die Dokumente jetzt unterschreiben?",
|
||||
"placeholder-mail-alert-you": "Nachfolgende Unterzeichner erhalten E-Mails, sobald Sie das Dokument unterschreiben.",
|
||||
"placeholder-alert-5": "Sie können jetzt unterschreiben oder es an Ihre E-Mail senden, um es später abzuschließen.",
|
||||
"placeholder-alert-6": "Bitte richten Sie den E-Mail-Adapter ein, um E-Mails zu senden!",
|
||||
"placeholder-alert-7": "Bitte wählen Sie einen Unterzeichner aus, um Platzhalter hinzuzufügen!",
|
||||
"email-subject": "E-Mail-Betreff",
|
||||
"email-body": "E-Mail-Inhalt",
|
||||
"email-placeholder": "E-Mail-Text eingeben",
|
||||
"reset-to-default": "Auf Standard zurücksetzen",
|
||||
"cutomize-email": "E-Mail anpassen",
|
||||
"customize-email": "E-Mail anpassen",
|
||||
"upgrade-to-customize-email": "Upgrade, um E-Mails anzupassen",
|
||||
"sign-url": "Signatur-URL",
|
||||
"mails-sent": "E-Mails gesendet",
|
||||
@@ -471,6 +533,7 @@
|
||||
"document-alert": "Dokument-Warnung",
|
||||
"owner-subscription-expired": "Das Abonnement des Besitzers ist abgelaufen.",
|
||||
"subscription-expired": "Abonnement abgelaufen",
|
||||
"owner-doesnt-have-paid-plan": "Der Inhaber hat keinen kostenpflichtigen Plan.",
|
||||
"alert-message": "Warnmeldung",
|
||||
"document-decline": "Dokument ablehnen",
|
||||
"decline-alert-1": "Sind Sie sicher, dass Sie dieses Dokument ablehnen möchten?",
|
||||
@@ -519,7 +582,7 @@
|
||||
"correct-password": "Bitte korrektes Passwort angeben",
|
||||
"decrypting-pdf": "PDF wird entschlüsselt, bitte warten...",
|
||||
"invalid-otp": "Ungültiger OTP",
|
||||
"user-not-found": "Benutzer nicht gefunden!",
|
||||
"user-not-found": "Benutzer nicht gefunden",
|
||||
"enter-otp-alert": "Bitte OTP eingeben!",
|
||||
"get-verification-code": "Verifizierungscode erhalten",
|
||||
"get-verification-code-2": "Sie erhalten einen Verifizierungscode per E-Mail",
|
||||
@@ -527,7 +590,7 @@
|
||||
"provide-your-details": "Bitte geben Sie Ihre Daten an",
|
||||
"provide-email": "Bitte E-Mail angeben.",
|
||||
"additional-info": "Zusätzliche Informationen",
|
||||
"signature-saved-alert": "Unterschrift erfolgreich gespeichert.",
|
||||
"signature-saved-alert": "erfolgreich gespeichert.",
|
||||
"saved-successfully": "Erfolgreich gespeichert.",
|
||||
"fill-field": "Bitte dieses Feld ausfüllen",
|
||||
"error-template": "Fehler: Vorlage nicht gefunden!",
|
||||
@@ -564,7 +627,7 @@
|
||||
"date": "Datum",
|
||||
"report-not-found": "Bericht nicht gefunden",
|
||||
"please-wait": "Bitte warten",
|
||||
"select-foler": "Ordner auswählen",
|
||||
"select-folder": "Ordner auswählen",
|
||||
"generated-on": "Erstellt am",
|
||||
"text-field": "Textfeld",
|
||||
"font-size": "Schriftgröße",
|
||||
@@ -583,7 +646,8 @@
|
||||
"tour-mssg": {
|
||||
"home-layout-1": "Sie haben sich erfolgreich angemeldet! Werfen wir einen Blick darauf.",
|
||||
"home-layout-2": "Um Dokumente zum Selbstunterzeichnen hochzuladen oder um Unterschriften anderer anzufordern, wählen Sie einfach die entsprechenden Schaltflächen.",
|
||||
"home-layout-3": "Sie sind bereit, {{appName}} zu verwenden! Wenn Sie Unterstützung benötigen, kontaktieren Sie uns gerne.",
|
||||
"home-layout-3": "Sie sind bereit, {{appName}} zu verwenden!",
|
||||
"home-layout-4": "Markiere uns auf",
|
||||
"generate-token": "Jetzt upgraden, um ein Produktions-API-Token zu generieren.",
|
||||
"opensign-drive-1": "Klicken Sie auf die Breadcrumb-Links, um einfach durch die Ordnerhierarchie zu navigieren und die Dokumente in jedem Ordner anzuzeigen.",
|
||||
"opensign-drive-2": "Klicken Sie auf die Schaltfläche Hinzufügen, um einen neuen Ordner oder ein neues Dokument zu erstellen.",
|
||||
@@ -593,27 +657,26 @@
|
||||
"opensign-drive-6": "Klicken Sie mit der rechten Maustaste auf ein Dokument, um Optionen wie Herunterladen, Umbenennen, Verschieben und Löschen anzuzeigen. Klicken Sie auf das Dokument, um es zu öffnen.",
|
||||
"opensign-drive-7": "Klicken Sie mit der rechten Maustaste auf einen Ordner, um Optionen anzuzeigen. Wählen Sie Umbenennen, um den Namen des Ordners zu ändern, oder klicken Sie auf den Ordner, um seinen Inhalt anzuzeigen.",
|
||||
"pdf-request-file-1": "Liste der Unterzeichner, die das Dokument noch unterschreiben müssen.",
|
||||
"pdf-request-file-2": "Klicken Sie auf die Platzhalter im Dokument, um zu unterschreiben. Sie sehen dann Optionen, Ihre Unterschrift zu zeichnen, einzugeben oder ein Bild hochzuladen.",
|
||||
"pdf-request-file-3": "Klicken Sie auf die Schaltflächen Ablehnen oder Fertig stellen, um durch Ihr Dokument zu navigieren. Verwenden Sie das Auslassungsmenü für weitere Optionen, einschließlich der Schaltfläche Herunterladen.",
|
||||
"pdf-request-file-2": "Klicken Sie auf eines der Felder im Dokument, um zu beginnen. Danach können Sie die erforderlichen Informationen eingeben.",
|
||||
"pdf-request-file-3": "Sobald Sie alle erforderlichen Felder ausgefüllt haben, klicken Sie auf „Fertigstellen“. Danach können Sie das unterschriebene Dokument herunterladen. Je nach den Einstellungen des Absenders erhalten Sie möglicherweise eine Kopie des abgeschlossenen Dokuments zusammen mit dem Abschlusszertifikat, sobald alle Empfänger unterschrieben haben.",
|
||||
"pdf-request-file-4": "Liste der Unterzeichner, die das Dokument bereits unterzeichnet haben.",
|
||||
"pdf-request-file-5": "Sie können auf Automatisch alles unterschreiben klicken, um automatisch an allen für Sie vorgesehenen Stellen zu unterschreiben. Überprüfen Sie das Dokument gründlich, bevor Sie diese Schaltfläche verwenden.",
|
||||
"pdf-request-file-6": "Bitte füllen Sie die Felder auf Seite {{pagenumbers}} aus, die alle zur einfachen Identifizierung in der gleichen Farbe hervorgehoben sind.",
|
||||
"placeholder-sign-1": "Wählen Sie einen Empfänger aus dieser Liste aus, um einen Platzhalter hinzuzufügen, an dem er unterschreiben soll. Der Platzhalter wird in der gleichen Farbe wie der Empfängername angezeigt, sobald Sie ihn im Dokument ablegen.",
|
||||
"placeholder-sign-1": "Wählen Sie einen Empfänger aus dieser Liste, um ein Widget hinzuzufügen. Das Widget wird in derselben Farbe wie der Empfängername angezeigt, sobald Sie es auf das Dokument ziehen.",
|
||||
"placeholder-sign-2": "Das Klicken auf die Schaltfläche 'Empfänger hinzufügen' ermöglicht es Ihnen, weitere Unterzeichner hinzuzufügen.",
|
||||
"placeholder-sign-3": "Klicken Sie auf diese Schaltfläche, um weitere Empfänger/Unterzeichner zum Dokument hinzuzufügen.",
|
||||
"placeholder-sign-4": "Ziehen Sie ein Feld in das Dokument oder klicken Sie darauf, um es hinzuzufügen.",
|
||||
"placeholder-sign-5": "Der PDF-Inhaltsbereich zeigt bereits die vorhandenen Platzhalter der Vorlage an. Diese Platzhalter entsprechen der Farbe des Empfängernamens, um sie leicht erkennbar zu machen.",
|
||||
"placeholder-sign-4": "Ziehen oder klicken Sie auf ein Widget, um es dem Dokument hinzuzufügen.",
|
||||
"placeholder-sign-5": "Der PDF-Inhaltsbereich zeigt bereits die vorhandenen Widgets der Vorlage an. Diese Widgets entsprechen der Farbe des Empfängernamens, um sie leicht erkennbar zu machen.",
|
||||
"placeholder-sign-6": "Mit einem Klick auf 'Weiter' wird das Dokument gespeichert. Im nächsten Schritt können Sie die E-Mails, die an die Empfänger versendet werden sollen, anpassen oder die Signaturlinks kopieren und diese selbst mit den Empfängern teilen.",
|
||||
"report-1": "Klicken Sie auf die Schaltfläche „Hinzufügen“, um eine neue Vorlage zu erstellen. Vorlagen sind wiederverwendbare Dokumente, mit denen schnell neue Dokumente mit derselben Struktur und unterschiedlichen Unterzeichnern erstellt werden können. Eine HR-Vorlage für die Einarbeitung könnte beispielsweise vordefinierte Rollen wie „Personalleiter“ und „Neuer Mitarbeiter“ enthalten. Bei jeder Verwendung der Vorlage können Sie die Rolle „Neuer Mitarbeiter“ verschiedenen neuen Mitarbeitern zuweisen, während die Rolle „Personalleiter“ unverändert bleibt. So wird ein nahtloser Einarbeitungsprozess für jeden neuen Mitarbeiter ermöglicht.",
|
||||
"redirect": "Klicken Sie auf die Schaltfläche 'Verwenden', um ein neues Dokument aus einer bestehenden Vorlage zu erstellen.",
|
||||
"bulksend": "Um schnell mehrere Dokumente mithilfe einer vorhandenen Vorlage zu versenden, indem Sie einfach die E-Mail-Adressen der Empfänger erstellen, klicken Sie auf die Schaltfläche ‚Massenversand‘",
|
||||
"option": "Dieses Menü zeigt weitere Optionen wie Bearbeiten und Löschen. Verwenden Sie die Schaltfläche 'Bearbeiten', um Unterzeichnerrollen hinzuzufügen, Felder zu ändern und Ihre Vorlage zu aktualisieren. Änderungen gelten für alle zukünftigen Dokumente, die aus dieser Vorlage erstellt werden, wirken sich jedoch nicht auf vorhandene Dokumente aus. Verwenden Sie die Schaltfläche 'Löschen', um die Vorlage zu entfernen.",
|
||||
"signyour-self-1": "Wählen und ziehen Sie Ihre bevorzugten Widgets auf das PDF, um Ihr Dokument vor dem Unterzeichnen anzupassen. Wählen Sie die perfekten Stellen für jede Anpassung aus, um das Dokument an Ihre Bedürfnisse anzupassen.",
|
||||
"bulksend": "Um schnell mehrere Dokumente mit einer vorhandenen Vorlage zu versenden, geben Sie einfach die E-Mail-Adressen der Empfänger ein und klicken Sie auf die Schaltfläche 'Massenversand'. Sie können bis zu 50 Empfänger erreichen.",
|
||||
"option": "Dieses Menü zeigt weitere Optionen wie Bearbeiten, Löschen, Umbenennen, Duplizieren, Teilen usw. <1>Klicken Sie hier</1>, um mehr über alle verfügbaren Optionen zu erfahren. <3>Hinweis: Änderungen an einer vorhandenen Vorlage gelten für alle zukünftigen Dokumente, die aus dieser Vorlage erstellt werden, betreffen jedoch nicht bereits versandte Dokumente.</3>",
|
||||
"signyour-self-1": "Wählen oder ziehen Sie Ihre bevorzugten Widgets auf das PDF, um Ihr Dokument vor der Unterzeichnung anzupassen. Platzieren Sie die Widgets an den perfekten Stellen, um das Dokument an Ihre Bedürfnisse anzupassen.",
|
||||
"signyour-self-2": "Ziehen Sie ein Widget irgendwo in diesen Bereich. Sie können es später in der Größe ändern und verschieben.",
|
||||
"template-placeholder-1": "Das Klicken auf die Schaltfläche 'Rolle hinzufügen' ermöglicht es Ihnen, verschiedene Unterzeichnerrollen hinzuzufügen. Sie können Benutzern diese Rollen in den folgenden Schritten zuweisen.",
|
||||
"template-placeholder-2": "Sobald Rollen hinzugefügt wurden, wählen Sie eine Rolle aus der Liste aus, um einen Platzhalter hinzuzufügen, an dem der Unterzeichner unterschreiben soll. Der Platzhalter wird in der gleichen Farbe wie der Rollenname angezeigt, sobald Sie ihn im Dokument ablegen.",
|
||||
"template-placeholder-3": "Ziehen Sie ein Feld in das Dokument oder klicken Sie darauf, um es hinzuzufügen.",
|
||||
"template-placeholder-4": "Ziehen Sie den Platzhalter für eine Rolle an eine beliebige Stelle im Dokument. Denken Sie daran, dass er in der gleichen Farbe wie der Name des Empfängers angezeigt wird, um die Zuordnung zu erleichtern.",
|
||||
"template-placeholder-5": "Das Klicken auf 'Weiter' speichert die aktuelle Vorlage. Nach dem Speichern werden Sie gefragt, ob Sie ein neues Dokument aus dieser Vorlage erstellen möchten.",
|
||||
"template-placeholder-2": "Nachdem Sie Rollen hinzugefügt haben, wählen Sie eine aus der Liste, um ein Widget für diesen Empfänger zu platzieren. Sie können entweder auf das Widget klicken oder es auf das Dokument ziehen. Sobald es platziert ist, wird das Widget in derselben Farbe wie die ausgewählte Rolle angezeigt.",
|
||||
"template-placeholder-3": "Das Klicken auf 'Weiter' speichert die aktuelle Vorlage. Nach dem Speichern werden Sie gefragt, ob Sie ein neues Dokument aus dieser Vorlage erstellen möchten.",
|
||||
"webhook-1": "Jetzt upgraden, um einen Webhook einzurichten",
|
||||
"Need your Signature": "Das Klicken auf diese Karte führt Sie zu einer Liste der Dokumente, die auf Ihre Überprüfung warten.",
|
||||
"Out for signatures": "Das Klicken auf diese Karte führt Sie zu einer Liste der Dokumente, die auf eine Signatur warten.",
|
||||
@@ -623,13 +686,13 @@
|
||||
"public-template": "Dieses Video zeigt Ihnen, wie Sie Ihr personalisiertes öffentliches Profil einrichten, z. B. 'https://opensign.me/Ihr-Benutzername'. Sie lernen auch, wie Sie Ihren Slogan anpassen und Ihre Vorlagen für öffentliche Signaturen verfügbar machen.",
|
||||
"allowModify-widgets": "Sie können jedes dieser Felder per Drag & Drop in das Dokument ziehen, zusätzlich zu den Feldern, die der Dokumentersteller bereits für Sie festgelegt hat."
|
||||
},
|
||||
"enter-email-plaholder": "E-Mail-Adresse hinzufügen und Enter drücken",
|
||||
"enter-email-placeholder": "E-Mail-Adresse hinzufügen und Enter drücken",
|
||||
"success-email-alert": "E-Mail erfolgreich gesendet!",
|
||||
"expired-doc-title": "Abgelaufenes Dokument",
|
||||
"expired-on-mssg": "Dieses Dokument ist am {{expiredDate}} abgelaufen und nicht mehr verfügbar zum Unterzeichnen.",
|
||||
"signature-validate-alert": "Bitte bestätigen Sie, dass Sie mindestens {{minRequiredCount}} Kontrollkästchen ausgewählt haben.",
|
||||
"signature-validate-alert-2": "Stellen Sie sicher, dass dieses Feld korrekt ausgefüllt ist und alle Anforderungen erfüllt.",
|
||||
"remaing-users": "Verbleibende Benutzer",
|
||||
"remaining-users": "Verbleibende Benutzer",
|
||||
"Role": "Rolle",
|
||||
"additional-users": "Bitte kaufen Sie zusätzliche Benutzer hinzu.",
|
||||
"Quantity-of-users": "Anzahl der Benutzer",
|
||||
@@ -643,7 +706,7 @@
|
||||
"select-date-format": "Wählen Sie ein Datumsformat",
|
||||
"quantity-of-credits": "Anzahl der Premium-Credits",
|
||||
"remaining-credits": "Verfügbare Premium-Credits:",
|
||||
"remaining-credits-help": "Verwenden Sie Premium-Credits für API-Dokumentensignaturen, Massenversand oder die Einbettung der OpenSign-Integration auf Ihrer Website. Sie haben {{allowedcredits}} enthaltene Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig.",
|
||||
"remaining-credits-help": "Verwenden Sie Premium-Credits für die API-Dokumentensignatur, den Massenversand oder die Einbettung der OpenSign-Integration auf Ihrer Website. Sie haben noch {{allowedcredits}} inkludierte Credits und {{addoncredits}} zusätzlich gekaufte Credits übrig. Wenn Sie einen Teams- oder Enterprise-Plan nutzen, können diese Credits von allen Benutzern in Ihrem Unternehmen gemeinsam verwendet werden, sofern der Kontoadministrator für Ihr Konto kein Limit festgelegt hat.",
|
||||
"additional-credits": "Bitte kaufen Sie Premium-Credits hinzu",
|
||||
"quota-err-quicksend": "Kontingent erreicht, Sie haben nicht genügend Credits.",
|
||||
"buy-credits": "Premium-Credits kaufen",
|
||||
@@ -651,7 +714,7 @@
|
||||
"rotate-left": "Nach links drehen",
|
||||
"rotate-alert-mssg": "Alle Widgets auf dieser Seite gehen verloren. Sind Sie sicher, dass Sie fortfahren möchten?",
|
||||
"templateid": "Vorlagen-ID",
|
||||
"bulk-send-subcription-alert": "Bitte upgraden Sie auf den Professional- oder Team-Plan, um den Massenversand zu nutzen.",
|
||||
"bulk-send-subscription-alert": "Bitte upgraden Sie auf den Professional- oder Team-Plan, um den Massenversand zu nutzen.",
|
||||
"generate-test-token": "Test-Token generieren",
|
||||
"regenerate-test-token": "Test-Token neu generieren",
|
||||
"help-test-token": "Dieses Token kann verwendet werden, um die APIs unter https://sandbox.opensignlabs.com/api/v1 zu testen und unbegrenzt Dokumente zu signieren. Beachten Sie, dass die Sandbox-API Ihre Dokumente mit selbstsignierten Zertifikaten signiert, die möglicherweise von Adobe nicht als gültig erkannt werden. Nach Abschluss Ihrer Tests können Sie auf einen unserer kostenpflichtigen Pläne upgraden, um ein Produktionstoken zu generieren.",
|
||||
@@ -662,7 +725,7 @@
|
||||
"public-template-mssg-1": "Um OpenSign in Ihr React- oder Next.js-Projekt zu integrieren, führen Sie einfach den folgenden Befehl aus:",
|
||||
"public-template-mssg-2": "Stellen Sie sicher, dass npm oder yarn in Ihrem Projekt eingerichtet ist. Wenn Sie Yarn verwenden, können Sie npm install durch yarn add @opensign/react ersetzen.",
|
||||
"public-template-mssg-3": "Benötigen Sie weitere Details oder Beispiele?",
|
||||
"public-template-mssg-4": "Besuchen Sie die",
|
||||
"public-template-mssg-4": "Besuchen Sie die ",
|
||||
"public-template-mssg-5": " npm für die neuesten Updates, detaillierte Dokumentationen und Versionshistorie.",
|
||||
"public-template-mssg-6": "Bevor Sie diesen Code-Schnipsel verwenden können, müssen Sie diese Vorlage öffentlich machen.",
|
||||
"public-template-mssg-7": "Bevor Sie einen öffentlichen Link generieren können, müssen Sie diese Vorlage öffentlich machen.",
|
||||
@@ -671,8 +734,9 @@
|
||||
"copied-code": "KOPIERT",
|
||||
"Installation": "Installation",
|
||||
"Usage": "Nutzung",
|
||||
"insufficient-credits": "Nicht genügend Signatur-Credits",
|
||||
"insufficient-credits-mssg": "Der Besitzer dieses Dokuments verfügt derzeit nicht über die notwendigen OpenSign-Credits, damit Sie unterzeichnen können. Bitte wenden Sie sich an den Besitzer, wenn Sie weitere Informationen benötigen.",
|
||||
"insufficient-credits": "Unzureichende Signaturguthaben. Bitte kontaktieren Sie {{ownerEmail}}",
|
||||
"insufficient-credits-title": "Nicht genügend Signatur-Credits",
|
||||
"insufficient-credits-mssg": "Der Besitzer dieses Dokuments verfügt derzeit nicht über die notwendigen {{appName}}-Credits, damit Sie unterzeichnen können. Bitte wenden Sie sich an den Besitzer, wenn Sie weitere Informationen benötigen.",
|
||||
"angular-npm-mssg-1": "Um OpenSign in Ihr Angular-Projekt zu integrieren, führen Sie einfach den folgenden Befehl aus:",
|
||||
"quota-mail-info-head": "Monatliches E-Mail-Limit",
|
||||
"quota-mail-info": "Um die Servicequalität aufrechtzuerhalten und Spam zu verhindern, erlaubt OpenSign bis zu 15 E-Mails pro Monat im kostenlosen Plan. Upgraden Sie jetzt für unbegrenzten E-Mail-Versand.",
|
||||
@@ -710,7 +774,7 @@
|
||||
"public-tour-message": "Die Vorlage muss öffentlich sein, bevor Sie einen teilbaren Link generieren können.",
|
||||
"add-user-template": "Sie müssen eine Rolle hinzufügen, bevor Sie Felder dafür hinzufügen können.",
|
||||
"pdf-uncompatible": "Diese PDF-Datei ist nicht kompatibel, bitte kontaktieren Sie {{appName}}",
|
||||
"text-field-tour": "Felder vom Typ 'Text' müssen im Voraus ausgefüllt werden, bevor das Dokument versendet wird. Wenn Sie möchten, dass die Unterzeichner Eingaben machen, verwenden Sie das Feld 'Texteingabe'.",
|
||||
"text-field-tour": "Felder zum Vorabfüllen müssen vor dem Senden des Dokuments ausgefüllt werden. Wenn Sie Eingaben von den Unterzeichnern benötigen, verwenden Sie stattdessen die Felder für Unterzeichner.",
|
||||
"attach-signer-tour": "Sie müssen einen Unterzeichner jeder Rolle zuweisen. Dies können Sie durch Klicken auf dieses Symbol tun. Sobald Sie einen Unterzeichner auswählen, wird er allen Feldern der zugehörigen Rolle zugewiesen, die in derselben Farbe erscheinen.",
|
||||
"allowed-signature-types": "Erlaubte Signaturtypen",
|
||||
"at-least-one-signature-type": "Mindestens ein Signaturtyp sollte aktiviert sein.",
|
||||
@@ -736,12 +800,15 @@
|
||||
"delete-page": "Seite löschen",
|
||||
"merge-pdf": "PDFs zusammenführen",
|
||||
"add-pages": "Seiten hinzufügen",
|
||||
"reorder-pages": "Seiten neu anordnen",
|
||||
"delete-alert": "Einzelne Seite kann nicht gelöscht werden.",
|
||||
"delete-alert-2": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
|
||||
"delete-note": "Hinweis: Sobald Sie diese Seite löschen, kann dies nicht rückgängig gemacht werden.",
|
||||
"Rotation-alert": "Seite drehen",
|
||||
"bulk-import": "Massenimport",
|
||||
"contacts-file": "Kontaktdatei (xlsx, csv)",
|
||||
"import-guideline": "Laden Sie eine CSV- oder Excel-Datei mit den Spalten Name, Email und optional Phone hoch. Es werden nur die ersten 100 Kontakte importiert.",
|
||||
"download-sample": "Beispieldatei herunterladen",
|
||||
"100-records-only": "Derzeit können Sie nur bis zu 100 Datensätze importieren.",
|
||||
"csv-excel-support-only": "Laden Sie eine Datei in einem der folgenden Formate hoch: CSV, XLSX oder XLS.",
|
||||
"contact-imported": "{{imported}} Kontakte wurden importiert. {{failed}} Kontakte konnten nicht importiert werden.",
|
||||
@@ -755,7 +822,7 @@
|
||||
"agree-p1": "Ich bestätige, dass ich die ",
|
||||
"agree-p2": "Offenlegung elektronischer Aufzeichnungen und Signaturen",
|
||||
"agree-p3": "gelesen und verstanden habe und der Verwendung elektronischer Aufzeichnungen und Signaturen zustimme.",
|
||||
"agrre-button": "Zustimmen & Fortfahren",
|
||||
"agrre-button": "Ich bestätige und stimme zu, fortzufahren",
|
||||
"term-cond-title": "Allgemeine Geschäftsbedingungen",
|
||||
"term-cond-h": "OFFENLEGUNG ELEKTRONISCHER AUFZEICHNUNGEN UND SIGNATUREN",
|
||||
"term-cond-p1": "Diese Offenlegung elektronischer Aufzeichnungen und Signaturen ('Offenlegung') ist eine Vereinbarung zwischen dem Dokumentersteller ('Sender') und dem Unterzeichner ('Ihnen'), bereitgestellt über die {{appName}} Plattform ('Plattform'). Durch das Signieren von Dokumenten über {{appName}} stimmen Sie den in dieser Offenlegung beschriebenen Bedingungen zu. Bitte lesen Sie sie sorgfältig durch, bevor Sie fortfahren.",
|
||||
@@ -801,7 +868,6 @@
|
||||
"js-snippet-msg-1": "Die TemplateId erhalten Sie von der",
|
||||
"js-snippet-msg-2": "Vorlagen verwalten",
|
||||
"js-snippet-msg-3": "Seite.",
|
||||
"agrrement-alert": "Um fortzufahren, müssen Sie der Offenlegung über elektronische Aufzeichnungen und Unterschriften zustimmen.",
|
||||
"webhook-already-exists": "Die URL existiert bereits! Bitte versuchen Sie eine andere.",
|
||||
"webhook-must-be-secure": "Die Webhook-URL muss sicher sein und https:// verwenden.",
|
||||
"provide-test-token": "Bitte generieren Sie einen API-Test-Token.",
|
||||
@@ -828,7 +894,7 @@
|
||||
"initial-type": "Ihre Initialen",
|
||||
"redirect-url": "Weiterleitungs-URL",
|
||||
"bulk-send": "Massenversand",
|
||||
"select-timezone": "Wählen Sie Ihre Zeitzone",
|
||||
"select-timezone": "Zeitzone",
|
||||
"current-time": "Aktuelle Uhrzeit",
|
||||
"email-help": "Aus Sicherheitsgründen dürfen Sie die E-Mail-Adresse nicht ändern. Bitte erstellen Sie ein weiteres kostenloses Konto mit der neuen E-Mail-Adresse.",
|
||||
"doc-sent": "Dokument erfolgreich gesendet.",
|
||||
@@ -848,5 +914,487 @@
|
||||
"draft-template-info-p1": "Um Ihre Vorlage öffentlich zu machen, muss sie entweder eine einzelne Rolle enthalten oder, wenn sie mehrere Rollen umfasst, müssen alle zusätzlichen Rollen bereits den Unterzeichnern zugewiesen sein. Die nicht zugewiesene öffentliche Rolle muss leer bleiben und an erster Stelle stehen.",
|
||||
"visit-below-link": "Besuchen Sie den untenstehenden Link, um mehr zu erfahren -",
|
||||
"storage-help": "Durch die Aktivierung von BYOC können Sie Ihren eigenen S3-Speicher verbinden, sodass Ihre Dateien vollständig unter Ihrer Kontrolle bleiben und keine externen Kopien gespeichert werden. Wenn Ihnen Datenautonomie wichtig ist, erwägen Sie ein Upgrade auf Teams, um diese Funktion freizuschalten.",
|
||||
"daily-quota-reached": "You’ve reached your daily quota. For assistance, please contact quotas@opensignlabs.com."
|
||||
}
|
||||
"daily-quota-reached": "Sie haben Ihr tägliches Kontingent erreicht. Für Unterstützung kontaktieren Sie bitte quotas@opensignlabs.com.",
|
||||
"enabled-signature-type": "Aktivierte Signaturtypen",
|
||||
"enabled-signature-type-help": "Die Einstellung 'Aktivierte Signaturtypen' bestimmt, welche Signaturoptionen in Ihrer Organisation verfügbar sind. Wenn Sie beispielsweise die Option 'Zeichnen' deaktivieren, wird sie den Mitgliedern Ihrer Organisation im Signatur-Widget nicht angezeigt, während die anderen drei Optionen weiterhin zugänglich bleiben.",
|
||||
"indexing-public-profile": "Erlaube die Indexierung des öffentlichen Profils durch Suchmaschinen",
|
||||
"user-created-successfully": "Benutzer erfolgreich erstellt.",
|
||||
"only-15-reminder-allowed": "Sie können bis zu 15 automatische Erinnerungen festlegen. Wenn zum Beispiel 'TimeToComplete' auf 15 Tage und 'RemindOnceInEvery' auf 1 Tag eingestellt ist, erreichen Sie das maximale Limit von 15 Erinnerungen. Passen Sie Ihre Einstellungen entsprechend an.",
|
||||
"rate-your-experience": "Wie war Ihre Erfahrung mit {{appName}}?",
|
||||
"thanks-for-feedback": "Danke für Ihr Feedback 🙏",
|
||||
"share-your-feedback": "Teilen Sie Ihr Feedback",
|
||||
"share-your-review": "Teilen Sie Ihre Bewertung",
|
||||
"please-select-rating": "Bitte wählen Sie eine Bewertung aus",
|
||||
"feedback-optional": "Feedback (optional)",
|
||||
"feedback-saved": "Feedback wurde gespeichert.",
|
||||
"feedback-save-error": "Feedback konnte nicht gespeichert werden, bitte versuchen Sie es erneut.",
|
||||
"date-format": "Datumsformat",
|
||||
"document-deleted": "Das Dokument wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
|
||||
"save-as-template-?": "Sind Sie sicher, dass Sie dieses Dokument als Vorlage speichern möchten?",
|
||||
"go-to-manage-templates": "Zu 'Vorlagen verwalten' gehen",
|
||||
"template-created": "Vorlage erstellt",
|
||||
"how-would-you-like-to-proceed?": "Wie möchten Sie fortfahren?",
|
||||
"failed-to-load-refresh-page": "Fehler beim Laden des Dokuments. Bitte versuchen Sie, diese Seite zu aktualisieren.",
|
||||
"document-has-been-signed": "Das Dokument wurde erfolgreich unterschrieben!",
|
||||
"document-has-been-signed-by-you": "Das Dokument wurde erfolgreich von Ihnen unterschrieben!",
|
||||
"participant-completed-signing": "Alle Teilnehmer haben den Signaturprozess abgeschlossen.",
|
||||
"you-will-receive-email-shortly": "✅ Das war's! Sie erhalten in Kürze eine Bestätigungs-E-Mail.",
|
||||
"please-provide-templateid": "Bitte geben Sie templateid an",
|
||||
"this-template-is-not-public": "Dieses template ist nicht öffentlich",
|
||||
"invalid-templateid": "Ungültige templateid",
|
||||
"contact-billing-at-opensign": "Um weitere Plätze hinzuzufügen, kontaktieren Sie bitte OpenSign™ unter <1>billing@opensignlabs.com</1> für Unterstützung.",
|
||||
"title-length-alert": "Der Titel darf höchstens 250 Zeichen lang sein.",
|
||||
"note-length-alert": "Die Notiz darf höchstens 200 Zeichen lang sein.",
|
||||
"description-length-alert": "Die Beschreibung darf höchstens 500 Zeichen lang sein.",
|
||||
"fix-&-resend-document": "Dokument korrigieren und erneut senden",
|
||||
"do-you-want-recreate-document?": "Dadurch wird ein Entwurf aus diesem Dokument mit allen vorhandenen Feldern erstellt. Sind Sie sicher, dass Sie dieses Dokument neu erstellen möchten?",
|
||||
"start-editing": "Bearbeitung starten",
|
||||
"unsaved-changes-discard-them?": "Sie haben ungespeicherte Änderungen. Verwerfen?",
|
||||
"yes-discard": "Ja, verwerfen",
|
||||
"LTV-enabled-signatures": "LTV-fähige Signaturen",
|
||||
"BETA": "BETA",
|
||||
"two-factor-authentication": "Zwei-Faktor-Authentifizierung",
|
||||
"2fa-help-text": "Die Zwei-Faktor-Authentifizierung fügt Ihrem Konto eine zusätzliche Sicherheitsebene hinzu, indem sie mehr als nur ein Passwort zum Anmelden erfordert.",
|
||||
"2fa-help-bullet1": "Erhöht die Sicherheit, indem sowohl Ihr Passwort als auch ein Bestätigungscode erforderlich sind.",
|
||||
"2fa-help-bullet2": "Der Bestätigungscode wird von einer Authentifizierungs-App auf Ihrem Gerät generiert.",
|
||||
"2fa-help-bullet3": "Schützt Ihr Konto, selbst wenn Ihr Passwort kompromittiert wurde.",
|
||||
"setup-2fa": "2FA einrichten",
|
||||
"setup-2fa-again": "2FA erneut einrichten",
|
||||
"2fa-setup-intro": "Schützen Sie Ihr Konto mit der Zwei-Faktor-Authentifizierung. Wenn aktiviert, müssen Sie bei jeder Anmeldung einen Code aus Ihrer Authentifizierungs-App eingeben.",
|
||||
"scan-qr-code": "QR-Code scannen",
|
||||
"scan-qr-instructions": "Verwenden Sie eine Authentifizierungs-App wie Google Authenticator, Microsoft Authenticator oder Authy, um diesen QR-Code zu scannen.",
|
||||
"manual-setup-instructions": "Können Sie den Code nicht scannen? Sie können Ihre Authentifizierungs-App manuell mit diesem geheimen Schlüssel einrichten:",
|
||||
"secret-key": "Geheimer Schlüssel",
|
||||
"copied-to-clipboard": "In die Zwischenablage kopiert",
|
||||
"copy-to-clipboard": "In die Zwischenablage kopieren",
|
||||
"recovery-codes": "Wiederherstellungscodes",
|
||||
"recovery-codes-instructions": "Speichern Sie diese Wiederherstellungscodes an einem sicheren Ort. Wenn Sie den Zugriff auf Ihre Authentifizierungs-App verlieren, können Sie einen dieser Einmal-Codes verwenden, um sich anzumelden.",
|
||||
"download-recovery-codes": "Wiederherstellungscodes herunterladen",
|
||||
"verification-code": "Bestätigungscode",
|
||||
"enter-code-from-authenticator-app": "Geben Sie den 6-stelligen Code aus Ihrer Authentifizierungs-App ein",
|
||||
"verification-code-required": "Bestätigungscode ist erforderlich",
|
||||
"verification-code-invalid": "Ungültiger Bestätigungscode. Bitte versuchen Sie es erneut.",
|
||||
"2fa-enabled": "2FA aktiviert",
|
||||
"2fa-enabled-successfully": "Zwei-Faktor-Authentifizierung erfolgreich aktiviert",
|
||||
"2fa-setup-complete": "Einrichtung abgeschlossen!",
|
||||
"2fa-setup-complete-instructions": "Ihr Konto ist jetzt mit der Zwei-Faktor-Authentifizierung geschützt. Sie müssen bei jeder Anmeldung einen Bestätigungscode eingeben.",
|
||||
"two-factor-verification": "Zwei-Faktor-Verifizierung",
|
||||
"enter-verification-code-instructions": "Geben Sie den 6-stelligen Bestätigungscode aus Ihrer Authentifizierungs-App ein, um fortzufahren.",
|
||||
"recovery-code": "Wiederherstellungscode",
|
||||
"enter-recovery-code-help": "Geben Sie einen Ihrer Wiederherstellungscodes ein",
|
||||
"recovery-code-required": "Wiederherstellungscode ist erforderlich",
|
||||
"use-verification-code-instead": "Stattdessen Bestätigungscode verwenden",
|
||||
"use-recovery-code-instead": "Stattdessen Wiederherstellungscode verwenden",
|
||||
"regenerate-2fa-remove-existing": "Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung neu generieren möchten? Diese Aktion entfernt Ihre bestehenden Authentifizierungseinstellungen.",
|
||||
"use-passkey": "Mit Passkey anmelden",
|
||||
"security-section": "Sicherheit",
|
||||
"passkey-authentication": "Passkey-Authentifizierung",
|
||||
"passkey-not-supported": "Ihr Browser oder Gerät unterstützt keine Passkey-Authentifizierung",
|
||||
"passkey-description": "Passkeys bieten eine stärkere, phishing-resistente Alternative zu Passwörtern. Sie können Ihren Fingerabdruck, Ihre Gesichtserkennung oder Ihre Geräte-PIN verwenden, um sich sicher anzumelden.",
|
||||
"passkey-tooltip": "Passkeys sind eine einfachere und sicherere Alternative zu Passwörtern. Sie verwenden biometrische Daten wie Fingerabdrücke oder Gesichtserkennung, die bereits auf Ihrem Gerät gespeichert sind.",
|
||||
"security-section-help": "Verwalten Sie Sicherheitsoptionen, einschließlich Passkeys und Authentifizierungsmethoden, um Ihr Konto zu schützen.",
|
||||
"passkey-register": "Passkey registrieren",
|
||||
"passkey-register-another": "Weiteren Passkey registrieren",
|
||||
"passkey-registered": "Passkey registriert",
|
||||
"passkey-registering": "Passkey wird registriert...",
|
||||
"passkey-registered-success": "Passkey erfolgreich registriert!",
|
||||
"passkey-registration-failed": "Passkey-Registrierung fehlgeschlagen",
|
||||
"passkey-auth-failed": "Passkey-Authentifizierung fehlgeschlagen",
|
||||
"passkey-missing-user-info": "Benutzerinformationen erforderlich",
|
||||
"passkeys-list": "Ihre Passkeys",
|
||||
"default-passkey": "Ihr Passkey",
|
||||
"passkey-rename": "Umbenennen",
|
||||
"passkey-delete": "Löschen",
|
||||
"passkey-rename-title": "Passkey umbenennen",
|
||||
"passkey-delete-title": "Passkey löschen",
|
||||
"passkey-delete-confirm": "Möchten Sie den Passkey \"{{name}}\" wirklich löschen?",
|
||||
"passkey-name": "Passkey-Name",
|
||||
"passkey-name-placeholder": "Geben Sie einen beschreibenden Namen für diesen Passkey ein",
|
||||
"passkey-renamed-success": "Passkey erfolgreich umbenannt",
|
||||
"passkey-deleted-success": "Passkey erfolgreich gelöscht",
|
||||
"passkey-rename-failed": "Passkey konnte nicht umbenannt werden",
|
||||
"passkey-delete-failed": "Passkey konnte nicht gelöscht werden",
|
||||
"processing": "Wird verarbeitet...",
|
||||
"today": "Heute",
|
||||
"yesterday": "Gestern",
|
||||
"days-ago": "Vor {{days}} Tagen",
|
||||
"verify-with-passkey": "Mit Passkey verifizieren",
|
||||
"verify-with-otp": "Mit OTP verifizieren",
|
||||
"verify-identity": "Identität verifizieren",
|
||||
"verify-account": "Verifizieren Sie Ihre Identität",
|
||||
"verification": "Verifizierung",
|
||||
"passkey-verification-failed": "Passkey-Verifizierung fehlgeschlagen",
|
||||
"security-auth-help": {
|
||||
"p1": "Administre la configuración de seguridad de su cuenta para mantener sus datos seguros. OpenSign admite métodos de autenticación avanzados para mejorar la protección de la cuenta.",
|
||||
"2fa-auth-help": "Agregue una capa adicional de seguridad activando 2FA. Esto requiere ingresar un código de verificación desde una aplicación autenticadora después de su contraseña.",
|
||||
"passkey-auth-help": "Use claves de acceso para iniciar sesión sin contraseña con verificación biométrica o basada en el dispositivo, proporcionando una seguridad sólida y comodidad."
|
||||
},
|
||||
"signer-already-present": "Unterzeichner bereits vorhanden",
|
||||
"kiosk-sign": "Kiosk-Unterschrift",
|
||||
"do-not-have-access-to-template": "Das template wurde gelöscht oder Sie haben keinen Zugriff. Bitte kontaktieren Sie den Absender.",
|
||||
"kiosk-info": "Kiosk Modus ermöglicht es Ihnen, persönliche Unterschriften schnell und effizient zu erfassen. Ideal für Messen, Veranstaltungen oder Laufkundschaft, bei denen alle Unterzeichner physisch anwesend sind. ",
|
||||
"learn-more": "Mehr erfahren",
|
||||
"finish-mssg": "Sind Sie sicher, dass Sie das Dokument abschließen möchten?",
|
||||
"review": "Überprüfen",
|
||||
"next-field": "Nächstes Feld",
|
||||
"required-mssg": "{{leftRequiredWidget}} von {{totalWidget}} Feldern übrig",
|
||||
"verify-document": "Dokument verifizieren",
|
||||
"verify-document-signature": "Dokumentensignatur überprüfen",
|
||||
"select-pdf-document": "PDF-Dokument auswählen",
|
||||
"selected-file": "Ausgewählte Datei",
|
||||
"verify-signature": "Signatur überprüfen",
|
||||
"verification-status": "Überprüfungsstatus",
|
||||
"verification-in-progress": "Überprüfung läuft...",
|
||||
"verification-results-will-appear-here": "Überprüfungsergebnisse werden hier angezeigt",
|
||||
"please-select-pdf": "Bitte wählen Sie eine gültige PDF-Datei aus",
|
||||
"please-select-file-to-verify": "Bitte wählen Sie eine Datei zur Überprüfung aus",
|
||||
"no-signature-found": "Keine Signatur im Dokument gefunden",
|
||||
"error-verifying-pdf": "Fehler beim Überprüfen der PDF",
|
||||
"signature-valid-basic": "Signatur ist gültig",
|
||||
"signature-invalid-basic": "Signatur ist ungültig",
|
||||
"all-signatures-verified-convincing": "Dokument überprüft: Alle Signaturen wurden erfolgreich validiert.",
|
||||
"some-signatures-invalid-basic": "Einige Signaturen sind ungültig",
|
||||
"no-signatures-processed": "Keine Signaturen verarbeitet",
|
||||
"unnamed-signature-field": "Unbenanntes Signaturfeld",
|
||||
"error-processing-signature": "Fehler beim Verarbeiten der Signatur",
|
||||
"signer-info-not-available": "Signaturinformationen nicht verfügbar",
|
||||
"cert-validity-not-checked": "Gültigkeit des Zertifikats nicht geprüft",
|
||||
"valid": "Gültig",
|
||||
"expired-or-not-yet-valid": "Abgelaufen oder noch nicht gültig",
|
||||
"valid-from": "Gültig von",
|
||||
"to": "bis",
|
||||
"signer": "Unterzeichner",
|
||||
"issuer": "Aussteller",
|
||||
"not-available": "Nicht verfügbar",
|
||||
"not-performed": "Nicht durchgeführt",
|
||||
"missing-acrofield-dict": "Fehlendes Acrofield-Wörterbuch",
|
||||
"signature-dictionary-not-found-or-invalid": "Signaturwörterbuch nicht gefunden oder ungültig",
|
||||
"missing-or-invalid-byterange": "Fehlender oder ungültiger ByteRange",
|
||||
"missing-or-invalid-contents": "Fehlender oder ungültiger Inhalt",
|
||||
"missing-signature-contents": "Fehlender Signaturinhalt",
|
||||
"invalid-signature-hex-format": "Ungültiges Signatur-Hex-Format",
|
||||
"unsupported-signature-format-not-signeddata": "Nicht unterstütztes Signaturformat - nicht SignedData",
|
||||
"signer-certificate-not-found": "Unterzeichnerzertifikat nicht gefunden",
|
||||
"no-certificates-in-signature": "Keine Zertifikate in der Signatur",
|
||||
"no-signer-info-in-pkcs7": "Keine Signaturinformationen in PKCS#7",
|
||||
"could-not-parse-signer-info": "Signaturinformationen konnten nicht analysiert werden",
|
||||
"not-calculated": "Nicht berechnet",
|
||||
"not-found-in-signature": "Nicht in Signatur gefunden",
|
||||
"readonly-error": "Das schreibgeschützte {{widgetName}}-Widget muss einen Standardwert haben oder kann optional gemacht werden.",
|
||||
"choose-one": "Wählen Sie eine aus",
|
||||
"search-templates": "Vorlagen durchsuchen…",
|
||||
"search-documents": "Dokumente suchen…",
|
||||
"search-contacts": "Kontakte durchsuchen…",
|
||||
"edit-draft": "Entwurf bearbeiten",
|
||||
"add-role-alert": "Bitte fügen Sie mindestens eine Rolle hinzu",
|
||||
"vertical": "Vertikal",
|
||||
"horizontal": "Horizontal",
|
||||
"billing": "Abrechnung",
|
||||
"console": "Konsole",
|
||||
"prefill-widget": "Vorausgefüllte Widgets",
|
||||
"action-prohibited": "Diese Aktion ist für Ihre E-Mail-Domain nicht erlaubt. Bitte wenden Sie sich an Ihren Administrator, um Hilfe zu erhalten.",
|
||||
"must-have-at-least-one-vacant-role": "Mindestens eine Rolle muss unzugewiesen sein, bevor Sie ein template auf 'public' setzen.",
|
||||
"remove-duplicate": "Bitte doppelte Option entfernen",
|
||||
"prefill-bulk-error": "Der Massenversand ist nicht erlaubt, wenn Prefill-Widgets hinzugefügt wurden. Bitte entfernen Sie die Prefill-Widgets, um fortzufahren.",
|
||||
"session-expired-title": "Sitzung abgelaufen",
|
||||
"access-denied": "Zugriff verweigert",
|
||||
"upgrade": "Upgrade",
|
||||
"do-not-access-app": "Sie haben keinen Zugriff auf diese Anwendung.",
|
||||
"do-not-have-access": "Sie haben keinen Zugriff.",
|
||||
"valid-email-alert": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||
"otp-not-validate": "OTP ist ungültig.",
|
||||
"domain-not-allowed": "Diese Domain ist nicht erlaubt",
|
||||
"atleast-one-recipient-alert": "Bitte fügen Sie mindestens einen Empfänger hinzu!",
|
||||
"incorrect-password-or-decryption-failed": "Falsches Passwort oder Entschlüsselung fehlgeschlagen.",
|
||||
"incorrect-password-for-file": "Falsches Passwort für Datei: {{file}}",
|
||||
"error-uploading-pdf": "Fehler beim Hochladen der PDF.",
|
||||
"provide-password": "Bitte geben Sie das Passwort an.",
|
||||
"only-pdf-allowed": "Nur PDF-Dateien sind erlaubt.",
|
||||
"invalid-username-password-region": "Ungültiger Benutzername/Passwort oder Region.",
|
||||
"pfx-extension-alert": "Bitte laden Sie eine Datei mit der Endung .pfx hoch.",
|
||||
"email-already-exist": "E-Mail existiert bereits",
|
||||
"branding": "Markenbildung",
|
||||
"branding-help": "Branding ermöglicht White-Labeling für Ihre App",
|
||||
"custom-sub-domain": "Benutzerdefinierte Subdomain",
|
||||
"app-name": "App Name",
|
||||
"provide-domain-name": "Es sind nur Subdomains erlaubt (z. B. app.deinedomain.com). Primär-/Root-Domains wie deinedomain.com werden nicht unterstützt.",
|
||||
"provide-app-name-help": "Geben Sie einen eindeutigen App-Namen an. Er wird als Ihr Markenname in der Anwendung sowie in allen E-Mails angezeigt, überall dort, wo das Branding angewendet wird.",
|
||||
"upload-app-logo": "Laden Sie ein Logo im .png-, .jpg- oder .svg-Format hoch. Empfohlene Größe: {{size}} Pixel (Seitenverhältnis 5:2) für beste Anzeigequalität.",
|
||||
"upload-app-favicon": "Laden Sie ein quadratisches Favicon-Bild (.ico, .png oder .svg) hoch. Empfohlene Größe: {{size}} Pixel für beste Browserkompatibilität.",
|
||||
"provide-app-name": "Geben Sie Ihren App-Namen an",
|
||||
"logo": "Logo",
|
||||
"prefill-unfilled-widget": "Die folgenden Pflichtfelder dürfen nicht leer sein: {{emptyWidget}}. Bitte füllen Sie diese aus, um fortzufahren.",
|
||||
"Dashboard": "Armaturenbrett",
|
||||
"Analytics": "Analytik",
|
||||
"Templates": "Vorlagen",
|
||||
"Need your sign": "Benötigt Ihre Unterschrift",
|
||||
"In Progress": "In Bearbeitung",
|
||||
"Completed": "Abgeschlossen",
|
||||
"Drafts": "Entwürfe",
|
||||
"Declined": "Abgelehnt",
|
||||
"Expired": "Abgelaufen",
|
||||
"Contactbook": "Kontaktbuch",
|
||||
"My Signature": "Meine Unterschrift",
|
||||
"API Token": "API-Token",
|
||||
"Webhook": "Webhook",
|
||||
"Preferences": "Einstellungen",
|
||||
"Teams": "Teams",
|
||||
"Users": "Benutzer",
|
||||
"Drive": "Drive",
|
||||
"Branding": "Markenbildung",
|
||||
"Mail": "Mail",
|
||||
"Storage": "Speicher",
|
||||
"Signing certificate": "Signierzertifikat",
|
||||
"General": "Allgemein",
|
||||
"Organizations": "Organisationen",
|
||||
"OrgAdmins": "OrgAdmins",
|
||||
"Debug Pdf": "PDF debuggen",
|
||||
"New Document": "Neues Dokument",
|
||||
"subscription": "Abonnement",
|
||||
"Draft document": "Dokumententwurf",
|
||||
"Draft template": "Vorlagenentwurf",
|
||||
"Public sign": "Öffentliche Signatur",
|
||||
"Signup": "Registrieren",
|
||||
"delete-contact": "Kontakt löschen",
|
||||
"total-records-found": "Gesamtanzahl gefundener Einträge: {{count}}",
|
||||
"Invalid-records-found": "Ungültige Einträge gefunden: {{records}}",
|
||||
"previous": "Zurück",
|
||||
"page-n-of-n": "Seite {{currentPage}} von {{totalPages}}",
|
||||
"import": "Importieren",
|
||||
"search": "Suchen",
|
||||
"viewed-on": "Angesehen am: {{ViewedOn}}",
|
||||
"signed-on": "Unterschrieben am: {{SignedOn}}",
|
||||
"hide": "Ausblenden",
|
||||
"show-more": "Mehr anzeigen",
|
||||
"browse-or-drag-to-replace-existing-file": "Durchsuchen oder per Drag & Drop eine neue Datei ziehen, um die vorhandene zu ersetzen",
|
||||
"optional-details": "Optionale Angaben",
|
||||
"hide-optional-details": "optionale Angaben ausblenden",
|
||||
"mail-adapter-subscription-alert": "Bitte upgraden Sie auf den Professional- oder Team-Plan, um den Mail-Adapter einzurichten.",
|
||||
"connect-to-mail": "Mit Gmail verbinden",
|
||||
"custom-smtp": "Benutzerdefiniertes SMTP",
|
||||
"default-smtp": "{{appName}} Standard-SMTP",
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"sender-email": "Absender-E-Mail",
|
||||
"username": "Benutzername",
|
||||
"use-default-mail-adapter": "Möchten Sie wirklich die Standard-Mailserver von {{appName}} verwenden, um Ihre Signaturanfragen zu versenden? Wir empfehlen, Ihre eigenen Gmail- oder SMTP-Server zu verwenden, um die Zustellbarkeit zu verbessern.",
|
||||
"verification-code-sent-registered-email": "Ein Bestätigungscode wurde an Ihre registrierte E-Mail-Adresse <1>{{useremail}}</1> gesendet. Bitte geben Sie den Code unten ein, um Ihre Einstellungen zu bestätigen.",
|
||||
"smpt-credentials": "SMTP-Zugangsdaten",
|
||||
"delete-account": "Konto löschen",
|
||||
"delete-account-que": "Sind Sie sicher, dass Sie Ihr Konto löschen möchten?",
|
||||
"delete-account-que-user": "Sie sind dabei, diesen Benutzer und alle zugehörigen Daten dauerhaft zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"user-deleted-successfully": "Benutzer und alle zugehörigen Daten wurden erfolgreich gelöscht.",
|
||||
"account-deletion-request-sent-via-mail": "Wir haben Ihnen einen Bestätigungslink per E-Mail gesendet. Genehmigen Sie die Anfrage, um die Löschung Ihres Kontos abzuschließen.",
|
||||
"type-exact-email-delete": "Geben Sie die genaue E-Mail ein, um das Löschen zu aktivieren",
|
||||
"email-does-not-match": "E-Mail stimmt nicht überein.",
|
||||
"please-type-to-confirm": "Bitte geben Sie <1>{{userEmail}}</1> ein, um zu bestätigen:",
|
||||
"email-settings": "E-Mail-Einstellungen",
|
||||
"email-settings-help": "Um eine bessere Zustellbarkeit Ihrer Signaturanfrage-E-Mails im Posteingang zu gewährleisten, können Sie Ihren eigenen E-Mail-Anbieter verbinden. Wählen Sie eine der folgenden Optionen:",
|
||||
"connect-to-gmail": "Mit Gmail verbinden: ",
|
||||
"connect-to-gmail-help": "Verwenden Sie Ihr Gmail-Konto, um Signaturanfrage-E-Mails direkt aus Ihrem eigenen Posteingang zu senden. Dies verbessert die Zustellraten und Zuverlässigkeit.",
|
||||
"connect-to-smtp": "Benutzerdefiniertes SMTP: ",
|
||||
"connect-to-smtp-help": "Verwenden Sie Ihren eigenen SMTP-Server, um E-Mails über Ihre Domain zu versenden. Diese Option gibt Ihnen die volle Kontrolle über Ihre E-Mail-Infrastruktur und verbessert die Zustellbarkeit sowie die Markenkonsistenz.",
|
||||
"connect-to-default": "Wenn Sie möchten, können Sie auch die Standard-Mailserver von {{appName}} verwenden, wir empfehlen jedoch für optimale Ergebnisse die Nutzung Ihrer eigenen.",
|
||||
"email-settings-redirect-message": "Diese Einstellung wurde von der Konsole zu den Haupteinstellungen → Präferenzen verschoben. Diese Seite wird in zukünftigen Versionen entfernt.",
|
||||
"go-to-preferences-menu": "Zum Menü 'Einstellungen' gehen",
|
||||
"document-download-filename-format": "Dateiname-Format für Dokument-Download",
|
||||
"preview": "Vorschau: ",
|
||||
"download-filename-format-help": "Wählen Sie, wie heruntergeladene PDFs benannt werden. Ihre Auswahl wird in Ihrem Profil gespeichert und in der gesamten App verwendet.",
|
||||
"delete-action-prohibited": "Diese Aktion ist nicht erlaubt. Bitte wenden Sie sich an Ihren Administrator, um die Löschung des Kontos zu beantragen.",
|
||||
"not-verified": "Nicht verifiziert",
|
||||
"verified": "Verifiziert",
|
||||
"expires": "Läuft ab",
|
||||
"fix-resend-error": "Sie können dieses Dokument nicht korrigieren und erneut senden, da es vorab ausgefüllte Widgets enthält.",
|
||||
"duplicate-template-error": "Sie können diese Vorlage nicht duplizieren, da sie vorab ausgefüllte Widgets enthält.",
|
||||
"save-as-template-error": "Dieses Dokument kann nicht als Vorlage gespeichert werden, da es vorab ausgefüllte Widgets enthält.",
|
||||
"redirecting-you-in": "Sie werden in {{redirectTimeLeft}} Sek. weitergeleitet...",
|
||||
"pdf-tools-tour": "Klicken Sie auf diese Schaltflächen, um Seiten hinzuzufügen, zu löschen, neu anzuordnen, zu drehen und zu zoomen.",
|
||||
"widgets": "Widgets",
|
||||
"prefill-tour": "Verwenden Sie diese Option, um Informationen im Voraus einzugeben, bevor Sie das Dokument an Empfänger senden.",
|
||||
"empty-prefill-error": "Die folgenden Pflichtfelder dürfen nicht leer bleiben:",
|
||||
"please-fill-out": "Bitte füllen Sie diese aus, um fortzufahren.",
|
||||
"custom-signing-certificate": "Benutzerdefiniertes Signaturzertifikat",
|
||||
"signing-certificate-help": "Sie können Ihr eigenes Dokumentensignaturzertifikat hochladen, das zum Signieren aller Ihrer Dokumente sowie Abschlusszertifikate verwendet wird. Die Zertifikatsdatei sollte ein P12-Zertifikat im PFX-Format sein.",
|
||||
"certificate-file-p12-in-PFX-format": "Zertifikatsdatei (p12-Zertifikat im PFX-Format)",
|
||||
"password-of-pfx-file": "Passwort der PFX-Datei eingeben",
|
||||
"update": "Aktualisieren",
|
||||
"use-default-certificate": "Standardzertifikat von {{appName}} verwenden",
|
||||
"upgrade-to-team-plan": "Auf Team-Plan upgraden",
|
||||
"setup-file-storage": "Dateispeicher einrichten",
|
||||
"save-and-activate": "Speichern & aktivieren",
|
||||
"logging-out-to-apply-settings": "Sie werden abgemeldet, um neue Einstellungen anzuwenden",
|
||||
"reconnect-to-default": "Erneut mit {{appName}} verbinden",
|
||||
"active-file-adapter": "Aktiver Dateiadapter",
|
||||
"file-adapter-unique-name": "Eindeutiger Name des Dateiadapters",
|
||||
"unique-name-for-file-adapter": "Eindeutigen Namen für Dateiadapter eingeben",
|
||||
"storage-provider": "Speicheranbieter",
|
||||
"enter-bucket-name": "Bucket-Namen eingeben",
|
||||
"enter-space-name": "Space-Namen eingeben",
|
||||
"enter-region-of-bucket": "Region des Buckets eingeben",
|
||||
"enter-region-of-space": "Region des Spaces eingeben",
|
||||
"enter-access-key": "Access Key eingeben",
|
||||
"enter-secret-access-key": "Secret Access Key eingeben",
|
||||
"otp-email": "Wir haben einen Bestätigungscode gesendet",
|
||||
"save-as-temp-warn": "Hinweis: Dieses Dokument enthält vorausgefüllte Widgets, die automatisch entfernt werden, da sie bereits im Basisdokument integriert sind.",
|
||||
"edit-document": "Dokument bearbeiten",
|
||||
"modify": "Ändern",
|
||||
"merge-certificate-to-pdf": "Zertifikat mit PDF zusammenführen",
|
||||
"merge-cc-to-pdf-help": {
|
||||
"p1": "Dies stellt sicher, dass das Abschlusszertifikat im endgültigen PDF-Dokument enthalten ist. Bitte beachten Sie jedoch, dass das Zertifikat nach dem Zusammenführen nicht mehr vom Hauptdokument getrennt werden kann.",
|
||||
"p2": "Wenn Sie sich entscheiden, nicht zusammenzuführen, wird das Abschlusszertifikat als separate PDF-Datei zusammen mit dem unterzeichneten Dokument bereitgestellt."
|
||||
},
|
||||
"read-only-date-error": "Das schreibgeschützte Datums-Widget muss ein Standarddatum haben.",
|
||||
"set-date": "Datum festlegen",
|
||||
"default-date": "Standarddatum",
|
||||
"set-today": "Unterzeichnungsdatum",
|
||||
"enter-name": "Name eingeben",
|
||||
"enter-email": "E-Mail eingeben",
|
||||
"subscribe-to-opensign-msg": "Abonnieren Sie {{appName}} und genießen Sie unbegrenzt kostenlose digitale Signaturen.",
|
||||
"duplicate-subscribe-msg": "Mit Duplizieren können Sie eine exakte Kopie der ausgewählten Vorlage erstellen, die Sie wiederverwenden oder ändern können, ohne das Original zu beeinflussen.",
|
||||
"save-as-template-msg": "Speichern Sie dieses Dokument als wiederverwendbare Vorlage, die Sie für zukünftige Dokumente erneut nutzen können.",
|
||||
"public-credit-alert": "Das Signieren über diesen Link verbraucht Ihre kostenlosen E-Mail-Credits. Als kostenloser Nutzer erhalten Sie 15 E-Mail-Credits pro Monat, um zu verhindern, dass Spammer unsere Systeme missbrauchen. Um höhere Limits und ununterbrochenen Zugriff zu genießen, abonnieren Sie die <1>OpenSign™-Bezahlpläne.</1>",
|
||||
"know-more-about": "Mehr erfahren über",
|
||||
"free-unlimited-signatures": "Kostenlose unbegrenzte Signaturen",
|
||||
"email-quota-updated": "Tägliches E-Mail-Kontingent erfolgreich aktualisiert.",
|
||||
"password-has-been-reset": "Passwort wurde zurückgesetzt.",
|
||||
"Unauthorized to reset your own password.": "Nicht berechtigt, Ihr eigenes Passwort zurückzusetzen.",
|
||||
"User not found or not allowed.": "Benutzer nicht gefunden oder nicht erlaubt.",
|
||||
"User not found.": "Benutzer nicht gefunden.",
|
||||
"Admin user tenant not found.": "Admin-Benutzer-Tenant nicht gefunden.",
|
||||
"User is not authenticated.": "Benutzer ist nicht authentifiziert.",
|
||||
"Please provide required parameters.": "Bitte geben Sie die erforderlichen Parameter an.",
|
||||
"reset-password": "Benutzerpasswort zurücksetzen",
|
||||
"enter-strong-password": "Geben Sie ein sicheres Passwort ein oder generieren Sie eines automatisch.",
|
||||
"autogenerate": "Automatisch generieren",
|
||||
"extend": "Verlängern",
|
||||
"console-general-help": "Um einzelnen Benutzern zu ermöglichen, ihre eigenen E-Mail-Vorlagen anzupassen, navigieren Sie zu",
|
||||
"console-mail-help": "Um unternehmensweite E-Mail-Vorlagen zu aktualisieren, gehen Sie zu",
|
||||
"admin-setting-disabled": "Diese Einstellung ist deaktiviert. Bitte wenden Sie sich für Unterstützung an den Administrator.",
|
||||
"company-email-settings": "Unternehmens-E-Mail-Einstellungen",
|
||||
"allow-email-template-individually": "E-Mail-Vorlagenanpassung für Benutzer individuell erlauben ",
|
||||
"user-email-template-general-help": "Wenn diese Option deaktiviert ist, können Benutzer ihre E-Mail-Vorlagen nicht mehr anpassen.",
|
||||
"disable-individual-user-smtp": "individuelle benutzer-smtp-einstellungen deaktivieren",
|
||||
"enable-individual-user-smtp": "individuelle benutzer-smtp-einstellungen aktivieren",
|
||||
"user-smtp-general-help": "Wenn deaktiviert, können Benutzer ihre eigenen SMTP-Einstellungen nicht konfigurieren. Alle E-Mails verwenden dann die globale SMTP-Konfiguration.",
|
||||
"global-smtp-settings": "Globale SMTP-Einstellungen",
|
||||
"global-smtp-settings-help-p1": "Diese Einstellungen gelten global für Ihr gesamtes {{appName}}-Konto. Wenn ein Benutzer keine eigenen E-Mail-Zustellungseinstellungen konfiguriert hat, verwendet das System automatisch die hier definierten globalen SMTP-Einstellungen.",
|
||||
"global-smtp-settings-help-p2": "Um eine bessere Zustellbarkeit aller Signaturanfragen sicherzustellen, können Sie Ihren bevorzugten E-Mail-Anbieter verbinden. Wählen Sie eine der folgenden Optionen:",
|
||||
"global-connect-to-gmail-help": "Verwenden Sie ein Gmail-Konto, um Signaturanfragen direkt aus dem Posteingang Ihrer Organisation zu senden. Dies verbessert die Zustellrate, Reputation und Zuverlässigkeit.",
|
||||
"global-connect-to-smtp-help": "Verwenden Sie Ihren eigenen SMTP-Server, um E-Mails über Ihre Domain zu senden. Dies bietet volle Kontrolle über Ihre E-Mail-Infrastruktur, verbessert die Zustellbarkeit und sorgt für konsistentes Branding.",
|
||||
"global-smtp-settings-help-p3": "Wenn keine individuellen Benutzereinstellungen konfiguriert sind, werden automatisch diese globalen Einstellungen angewendet.",
|
||||
"hide-text-with-asterisks": "Text mit Sternchen ausblenden",
|
||||
"hide-text-with-asterisks-help": "Der Text im unterzeichneten Dokument wird ausgeblendet, bleibt jedoch für den Dokumentabsender (Eigentümer) über sein {{appName}}-Konto zugänglich.",
|
||||
"document-id": "Dokument-ID",
|
||||
"sent-date": "Sendedatum",
|
||||
"status": "Status",
|
||||
"signer-form-data-for": "Formulardaten für Unterzeichner: {{signerName}} <{{signerEmail}}>",
|
||||
"widget-name": "Widget-Name",
|
||||
"widget-responses": "Widget-Antworten",
|
||||
"formdata-download-note": "Hinweis: Signaturen, Stempel, Initialen und Bilder werden nicht exportiert.",
|
||||
"export-all-formdata-error": "Sie können die Formulardaten nicht exportieren, da die Vorlage doppelte Widget-Namen enthält. Bitte erstellen Sie eine neue Vorlage, um Formulardaten exportieren zu können.",
|
||||
"export-formdata-error": "Sie können die Formulardaten nicht exportieren, da das Dokument doppelte Widget-Namen enthält. Bitte erstellen Sie ein neues Dokument ohne doppelte Widget-Namen, um Formulardaten exportieren zu können.",
|
||||
"duplicate-widget-name-error": "Dieser Widget-Name ist bereits einem anderen Unterzeichner zugewiesen. Bitte wählen Sie einen anderen Namen.",
|
||||
"duplicate-template-widget-error": "Sie können diese Vorlage nicht duplizieren, da sie doppelte Widget-Namen enthält.",
|
||||
"credit-alert": "Kreditwarnung",
|
||||
"document-form-data": "Dokumenten-Formulardaten",
|
||||
"view-formdata-paid-title-error": "Die Funktion 'Formulardaten anzeigen' ermöglicht es Ihnen, die von Unterzeichnern ausgefüllten Widget-Daten auszudrucken und herunterzuladen.",
|
||||
"view-formdata-paid-error": "Hinweis: Die Funktion 'Formulardaten anzeigen' ist nur für Dokumente verfügbar, die ab Version v2.32.0 erstellt wurden und keine doppelten Widget-Namen enthalten.",
|
||||
"download-all-formdata-paid-title-error": "Die Funktion 'Alle Formulardaten herunterladen' ermöglicht es Ihnen, die von Unterzeichnern ausgefüllten Widget-Daten für alle aus der Vorlage erstellten Dokumente herunterzuladen.",
|
||||
"download-all-formdata-paid-error": "Hinweis: Die Funktion 'Alle Formulardaten herunterladen' ist nur für Vorlagen verfügbar, die ab Version v2.32.0 erstellt wurden und keine doppelten Widget-Namen enthalten.",
|
||||
"api-token-not-found": "API-Token nicht gefunden.",
|
||||
"sub-head-api-credits": "Legen Sie die maximale Anzahl von API-Credits fest, die dieser Benutzer verbrauchen darf.",
|
||||
"consumed-credits": "Verbrauchte Credits",
|
||||
"allowed-api-credits": "Zulässige API-Credits",
|
||||
"allowed-api-credits-help": "Die zulässigen Credits sollten größer oder gleich den verbrauchten Credits sein.",
|
||||
"Enter-allowed-api-credits": "Zulässige API-Credits eingeben",
|
||||
"login-to-sandbox": "Im Sandbox-Modus anmelden",
|
||||
"your-credits-usage": "Ihr Kreditverbrauch",
|
||||
"remaining": "verbleibend",
|
||||
"used": "verwendet",
|
||||
"your-credits-usage-help": "Dies ist die Anzahl der Credits, die mit Ihrem API-Schlüssel verbraucht wurden.",
|
||||
"api-credits-updated": "API-Credits wurden erfolgreich zugewiesen.",
|
||||
"min-date": "Mindestdatum",
|
||||
"max-date": "Höchstdatum",
|
||||
"invalid-min-date": "Ungültiges Mindestdatum.",
|
||||
"min-less-max": "Das Mindestdatum muss kleiner oder gleich dem Höchstdatum sein.",
|
||||
"invalid-max-date": "Ungültiges Höchstdatum.",
|
||||
"max-less-min": "Das Höchstdatum muss größer oder gleich dem Mindestdatum sein.",
|
||||
"date-range": "Datumsbereich",
|
||||
"date-range-help": "Legen Sie ein Mindest- und/oder Höchstdatum fest, um die Auswahl einzuschränken. Das gewählte Datum muss innerhalb dieses Bereichs liegen.",
|
||||
"option-disabled-by-owner": "Der Dokumentersteller hat diese Option deaktiviert",
|
||||
"pen-colors": "Stiftfarben",
|
||||
"pen-colors-help": "Wählen Sie aus, welche Stiftfarben für Signatur- und Initialen-Widgets verfügbar sind. Aktivieren Sie ein Kontrollkästchen, um eine Farbe anzuzeigen, und deaktivieren Sie es, um sie auszublenden.",
|
||||
"security-key": "Sicherheitsschlüssel",
|
||||
"remove-limit": "Limit entfernen",
|
||||
"removed-api-credits": "Das API-Credit-Limit wurde entfernt.",
|
||||
"enable-authentication": "Authentifizierung aktivieren",
|
||||
"working": "In Bearbeitung",
|
||||
"test-security-key": "Sicherheitsschlüssel testen",
|
||||
"generate": "Generieren",
|
||||
"regenerate": "Neu generieren",
|
||||
"signing-date": "Signierdatum",
|
||||
"date-widget": "Datums-Widget",
|
||||
"mail-failed": "Beim Senden der E-Mail ist etwas schiefgelaufen. Bitte versuchen Sie es erneut.",
|
||||
"mail-status-head": "Nächster Schritt: Ihr Dokument unterschreiben?",
|
||||
"send-to-email": "Per E-Mail senden",
|
||||
"date-pref-help-sub-title": "Mit den Datums-Widget-Einstellungen können Sie die Standardkonfiguration für das Datums-Widget festlegen. Diese Standards werden automatisch angewendet, wenn Sie ein neues Datums-Widget zu einem Dokument oder einer Vorlage hinzufügen. Sie können jedes Datums-Widget jederzeit individuell anpassen, indem Sie auf das Zahnradsymbol klicken.",
|
||||
"date-pref-help-format": "Wählen Sie das standardmäßige Datumsformat für Datums-Widgets.",
|
||||
"date-pref-help-default-date": "Legen Sie einen Standard-Datumswert für Datums-Widgets fest.",
|
||||
"date-pref-help-signing-date": "Wenn aktiviert, verwendet das Datums-Widget automatisch das Datum, an dem der Unterzeichner das Dokument signiert.",
|
||||
"date-pref-help-read-only": "Wenn aktiviert, ist das Datums-Widget für den Unterzeichner schreibgeschützt.",
|
||||
"upload-csv": "CSV hochladen",
|
||||
"file-selected": "Datei ausgewählt",
|
||||
"bulk-send-csv-helper": "Verwenden Sie Header für Unterzeichnerrollen (Rollen-E-Mail/Name/Telefon) und Vorabfüllwerte (Vorabfüllung: Feldname). Erforderliche Vorabfüllfelder müssen ausgefüllt sein.",
|
||||
"bulk-send-csv-helper-note": "Hinweis: Für Vorabfüllfelder vom Typ Datum in der CSV muss der Wert in folgendem Format angegeben werden: {{value}} (Beispiel: {{example}}).",
|
||||
"bulk-send-csv-summary": "{{total}} Zeilen aus der CSV importiert.",
|
||||
"bulk-send-csv-missing-headers": "Erforderliche CSV-Header fehlen. Rollen: {{roles}}.",
|
||||
"bulk-send-no-records": "Bitte fügen Sie vor dem Senden mindestens einen Empfänger hinzu.",
|
||||
"csv-only-support": "Es werden nur CSV-Dateien unterstützt.",
|
||||
"csv-no-records": "Keine Datensätze in der CSV gefunden.",
|
||||
"select-at-least-option": "Bitte wählen Sie mindestens eine Option aus.",
|
||||
"draw-required": "Bitte zeichnen Sie in das Feld.",
|
||||
"enter-smtp-username": "SMTP-Benutzernamen eingeben",
|
||||
"enter-webhook-url": "Webhook-URL eingeben",
|
||||
"enter-user-name": "Benutzernamen eingeben",
|
||||
"enter-tagline": "Slogan eingeben",
|
||||
"enter-password-or-click-autogenerate": "Passwort eingeben oder auf „Automatisch generieren“ klicken",
|
||||
"enter-daily-email-quota": "Tägliches E-Mail-Kontingent eingeben",
|
||||
"enter-custom-regular-expression": "Benutzerdefinierten regulären Ausdruck eingeben",
|
||||
"enter-value": "{{value}} eingeben",
|
||||
"are-you-sure-want-to-send-document": "Sind Sie sicher, dass Sie das Dokument senden möchten?",
|
||||
"favicon": "Favicon",
|
||||
"delete-two-factor-authentication": "Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung löschen möchten?",
|
||||
"paste-html-here": "HTML hier einfügen",
|
||||
"open-email-builder": "Sie können eine E-Mail-Vorlage mit der <1>E-Mail-Builder</1>-Plattform erstellen und den HTML-Code kopieren.",
|
||||
"contact-saved": "Kontakt erfolgreich gespeichert.",
|
||||
"remove-limit-of-user": "Sind Sie sicher, dass Sie das Limit dieses Benutzers aufheben möchten? Der Benutzer kann dann alle in Ihrer Organisation verfügbaren Premium-Credits nutzen, die unter allen Benutzern geteilt werden.",
|
||||
"current-expiry-date": "Aktuelles Ablaufdatum",
|
||||
"reminder-error": "„Einmal alle (Tage) erinnern“ darf die „Zeit zur Fertigstellung (Tage)“ nicht überschreiten.",
|
||||
"showing-to-of-records": "Zeige {{record}} bis {{recordPerPage}} von {{totalRows}} Einträgen",
|
||||
"records-per-page": "Einträge pro Seite",
|
||||
"email-not-found-in-row": "E-Mail in Zeile {{row}} nicht gefunden.",
|
||||
"invalid-email-found-in-row": "Ungültige E-Mail in Zeile {{row}} gefunden: {{email}}",
|
||||
"duplicate-email-found-in-row": "Doppelte E-Mail in Zeile {{row}} gefunden: {{email}}",
|
||||
"prefill": "Vorabfüllung",
|
||||
"recipients-&-data": "Empfänger & Daten",
|
||||
"response": "Antwort",
|
||||
"prefill-tab-help": "Dies sind die gemeinsamen Vorlagenfelder, die auf alle Dokumente in diesem Massenversand angewendet werden.",
|
||||
"recipient-tab-help-p1": "Im nächsten Schritt werden Sie:",
|
||||
"recipient-tab-help-p2": "Eine CSV mit Empfängerdetails hochladen oder",
|
||||
"recipient-tab-help-p3": "Empfängerinformationen manuell eingeben",
|
||||
"recipient-tab-help-p4": "Jeder Empfänger erhält sein eigenes Dokument, das aus dieser Vorlage generiert wird.",
|
||||
"response-tab-help": "Überprüfen Sie Ihre Antworten.",
|
||||
"summary": "Zusammenfassung",
|
||||
"prefill-fields": "Vorabfüllfelder",
|
||||
"documents": "Dokumente",
|
||||
"message": "Nachricht",
|
||||
"formula-help-p1": "Erfassen Sie numerische Eingaben und führen Sie dynamische Berechnungen mit Formeln durch.",
|
||||
"example": "Beispiel",
|
||||
"formula-help-p2": "Um eine Summe in number-3 anzuzeigen, verwenden Sie {{number-1}} + {{number-2}}.",
|
||||
"formula-help-p3": "Um einen Wert in number-4 zu berechnen, verwenden Sie ({{number-1}} * {{number-2}}) / {{number-3}}.",
|
||||
"formula-help-p4": "Jedes Zahlen-Widget muss einen eindeutigen Namen haben (z. B. number-1), da Formeln auf diesen Bezeichnern basieren.",
|
||||
"note-consume-premium-credits": "Hinweis: Diese Funktion verbraucht Premium-Credits.",
|
||||
"note-consume-premium-credits-on-document-send": "Hinweis: Diese Funktion verbraucht Premium-Credits. Sie werden jedes Mal verbraucht, wenn ein Dokument versendet wird – unabhängig davon, ob es unterschrieben wird oder nicht.",
|
||||
"webhook-authentication-help": "{{appName}} sichert jeden Webhook, indem eine HMAC-SHA256-Signatur mit Ihrem Webhook-Sicherheitsschlüssel und dem unveränderten Request-Payload generiert wird. Diese Signatur wird im Header x-webhook-signature gesendet. Ihr Server muss dieselbe Signatur mit dem empfangenen Payload und Ihrem geheimen Schlüssel generieren. Stimmen beide Signaturen überein, ist die Webhook-Anfrage authentisch und wurde nicht verändert.",
|
||||
"invalid-widget-found-in-row": "Ungültiger Wert in Zeile {{row}} für Widget {{widget}} erkannt."
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
Name,Email,Phone,Company,JobTitle
|
||||
John Doe,john@example.com,1234567890,abc firm,dev
|
||||
Jane Smith,jane@example.com,9876543210,xyz firm,manager
|
||||
Foo Bar,foo@example.com,5555555555,xyz firm,director
|
||||
|
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
||||
@@ -1,183 +0,0 @@
|
||||
import React, { useState, useEffect, lazy } from "react";
|
||||
import { Routes, Route, BrowserRouter } from "react-router";
|
||||
import { pdfjs } from "react-pdf";
|
||||
import Login from "./pages/Login";
|
||||
import Form from "./pages/Form";
|
||||
import Report from "./pages/Report";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import HomeLayout from "./layout/HomeLayout";
|
||||
import PageNotFound from "./pages/PageNotFound";
|
||||
import ValidateRoute from "./primitives/ValidateRoute";
|
||||
import Validate from "./primitives/Validate";
|
||||
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
|
||||
import SignYourSelf from "./pages/SignyourselfPdf";
|
||||
import DraftDocument from "./components/pdf/DraftDocument";
|
||||
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
||||
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
||||
import LazyPage from "./primitives/LazyPage";
|
||||
import Loader from "./primitives/Loader";
|
||||
import UserList from "./pages/UserList";
|
||||
import { serverUrl_fn } from "./constant/appinfo";
|
||||
import DocSuccessPage from "./pages/DocSuccessPage";
|
||||
import ValidateSession from "./primitives/ValidateSession";
|
||||
const DebugPdf = lazy(() => import("./pages/DebugPdf"));
|
||||
const ForgetPassword = lazy(() => import("./pages/ForgetPassword"));
|
||||
const GuestLogin = lazy(() => import("./pages/GuestLogin"));
|
||||
const ChangePassword = lazy(() => import("./pages/ChangePassword"));
|
||||
const UserProfile = lazy(() => import("./pages/UserProfile"));
|
||||
const Opensigndrive = lazy(() => import("./pages/Opensigndrive"));
|
||||
const ManageSign = lazy(() => import("./pages/Managesign"));
|
||||
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
|
||||
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
|
||||
const Preferences = lazy(() => import("./pages/Preferences"));
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[100vh]">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
function App() {
|
||||
const [isloading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
handleCredentials();
|
||||
}, []);
|
||||
|
||||
const handleCredentials = () => {
|
||||
const appId = process.env.REACT_APP_APPID
|
||||
? process.env.REACT_APP_APPID
|
||||
: "opensign";
|
||||
const baseurl = serverUrl_fn();
|
||||
try {
|
||||
localStorage.setItem("baseUrl", `${baseurl}/`);
|
||||
localStorage.setItem("parseAppId", appId);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log("err ", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-base-200">
|
||||
{isloading ? (
|
||||
<AppLoader />
|
||||
) : (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<ValidateRoute />}>
|
||||
<Route exact path="/" element={<Login />} />
|
||||
<Route
|
||||
path="/addadmin"
|
||||
element={<LazyPage Page={AddAdmin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/upgrade-2.1"
|
||||
element={<LazyPage Page={UpdateExistUserAdmin} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route element={<Validate />}>
|
||||
<Route
|
||||
path="/load/template/:templateId"
|
||||
element={<TemplatePlaceholder />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/load/placeholdersign/:docId"
|
||||
element={<PlaceHolderSign />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/load/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="/loadmf/signmicroapp/login/:id/:userMail/:contactBookId/:serverUrl"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/login/:id/:userMail/:contactBookId/:serverUrl"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/login/:base64url"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
|
||||
<Route
|
||||
path="/forgetpassword"
|
||||
element={<LazyPage Page={ForgetPassword} />}
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ValidateSession>
|
||||
<HomeLayout />
|
||||
</ValidateSession>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
path="/changepassword"
|
||||
element={<LazyPage Page={ChangePassword} />}
|
||||
/>
|
||||
<Route path="/form/:id" element={<Form />} />
|
||||
<Route path="/report/:id" element={<Report />} />
|
||||
<Route path="/dashboard/:id" element={<Dashboard />} />
|
||||
<Route
|
||||
path="/profile"
|
||||
element={<LazyPage Page={UserProfile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/drive"
|
||||
element={<LazyPage Page={Opensigndrive} />}
|
||||
/>
|
||||
<Route
|
||||
path="/managesign"
|
||||
element={<LazyPage Page={ManageSign} />}
|
||||
/>
|
||||
<Route
|
||||
path="/template/:templateId"
|
||||
element={<TemplatePlaceholder />}
|
||||
/>
|
||||
{/* signyouself route with no rowlevel data using docId from url */}
|
||||
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
|
||||
{/* draft document route to handle and navigate route page accordiing to document status */}
|
||||
<Route path="/draftDocument" element={<DraftDocument />} />
|
||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||
<Route
|
||||
path="/placeHolderSign/:docId"
|
||||
element={<PlaceHolderSign />}
|
||||
/>
|
||||
{/* for user signature (need your sign route) with row level data */}
|
||||
<Route path="/pdfRequestFiles" element={<PdfRequestFiles />} />
|
||||
{/* for user signature (need your sign route) with no row level data */}
|
||||
<Route
|
||||
path="/pdfRequestFiles/:docId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
{/* recipient signature route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
<Route path="/users" element={<UserList />} />
|
||||
<Route
|
||||
path="/preferences"
|
||||
element={<LazyPage Page={Preferences} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/success" element={<DocSuccessPage />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { lazyWithRetry, hideUpgradeProgress } from "./utils";
|
||||
import { Routes, Route, BrowserRouter } from "react-router";
|
||||
import { pdfjs } from "react-pdf";
|
||||
import Form from "./pages/Form";
|
||||
import Report from "./pages/Report";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import HomeLayout from "./layout/HomeLayout";
|
||||
import PageNotFound from "./pages/PageNotFound";
|
||||
import ValidateRoute from "./primitives/ValidateRoute";
|
||||
import Validate from "./primitives/Validate";
|
||||
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
|
||||
import SignYourSelf from "./pages/SignyourselfPdf";
|
||||
import DraftDocument from "./components/pdf/DraftDocument";
|
||||
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
||||
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
||||
import Lazy from "./primitives/LazyPage";
|
||||
import Loader from "./primitives/Loader";
|
||||
import UserList from "./pages/UserList";
|
||||
import { serverUrl_fn } from "./constant/appinfo";
|
||||
import DocSuccessPage from "./pages/DocSuccessPage";
|
||||
import DragProvider from "./components/DragProivder";
|
||||
import Title from "./components/Title";
|
||||
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
|
||||
const ForgetPassword = lazyWithRetry(() => import("./pages/ForgetPassword"));
|
||||
const GuestLogin = lazyWithRetry(() => import("./pages/GuestLogin"));
|
||||
const ChangePassword = lazyWithRetry(() => import("./pages/ChangePassword"));
|
||||
const UserProfile = lazyWithRetry(() => import("./pages/UserProfile"));
|
||||
const Opensigndrive = lazyWithRetry(() => import("./pages/Opensigndrive"));
|
||||
const ManageSign = lazyWithRetry(() => import("./pages/Managesign"));
|
||||
const AddAdmin = lazyWithRetry(() => import("./pages/AddAdmin"));
|
||||
const UpdateExistUserAdmin = lazyWithRetry(
|
||||
() => import("./pages/UpdateExistUserAdmin")
|
||||
);
|
||||
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
|
||||
const Login = lazyWithRetry(() => import("./pages/Login"));
|
||||
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
|
||||
const EmailBuilder = lazyWithRetry(() => import("./pages/EmailBuilder"));
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[100vh]">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
function App() {
|
||||
const [isloading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
// initialize creds
|
||||
const id = process.env.REACT_APP_APPID ?? "opensign";
|
||||
localStorage.setItem("parseAppId", id);
|
||||
localStorage.setItem("baseUrl", `${serverUrl_fn()}/`);
|
||||
hideUpgradeProgress();
|
||||
localStorage.removeItem("showUpgradeProgress");
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-base-200">
|
||||
{isloading ? (
|
||||
<AppLoader />
|
||||
) : (
|
||||
<BrowserRouter>
|
||||
<Title />
|
||||
<Routes>
|
||||
<Route element={<ValidateRoute />}>
|
||||
<Route exact path="/" element={<Lazy Page={Login} />} />
|
||||
<Route path="/addadmin" element={<Lazy Page={AddAdmin} />} />
|
||||
<Route
|
||||
path="/upgrade-2.1"
|
||||
element={<Lazy Page={UpdateExistUserAdmin} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route element={<Validate />}>
|
||||
<Route
|
||||
exact
|
||||
path="/load/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="/login/:base64url"
|
||||
element={<Lazy Page={GuestLogin} />}
|
||||
/>
|
||||
<Route path="/debugpdf" element={<Lazy Page={DebugPdf} />} />
|
||||
<Route
|
||||
path="/forgetpassword"
|
||||
element={<Lazy Page={ForgetPassword} />}
|
||||
/>
|
||||
<Route element={<HomeLayout />}>
|
||||
<Route path="/users" element={<UserList />} />
|
||||
<Route
|
||||
path="/changepassword"
|
||||
element={<Lazy Page={ChangePassword} />}
|
||||
/>
|
||||
<Route path="/form/:id" element={<Form />} />
|
||||
<Route path="/report/:id" element={<Report />} />
|
||||
<Route path="/dashboard/:id" element={<Dashboard />} />
|
||||
<Route path="/profile" element={<Lazy Page={UserProfile} />} />
|
||||
<Route path="/drive" element={<Lazy Page={Opensigndrive} />} />
|
||||
<Route path="/managesign" element={<Lazy Page={ManageSign} />} />
|
||||
<Route
|
||||
path="/template/:templateId"
|
||||
element={<DragProvider Page={TemplatePlaceholder} />}
|
||||
/>
|
||||
{/* signyouself route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/signaturePdf/:docId"
|
||||
element={<DragProvider Page={SignYourSelf} />}
|
||||
/>
|
||||
{/* draft document route to handle and navigate route page according to document status */}
|
||||
<Route
|
||||
path="/draftDocument"
|
||||
element={<DragProvider Page={DraftDocument} />}
|
||||
/>
|
||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||
<Route
|
||||
path="/placeHolderSign/:docId"
|
||||
element={<DragProvider Page={PlaceHolderSign} />}
|
||||
/>
|
||||
{/* recipient signature route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
/>
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
/>
|
||||
<Route
|
||||
path="/verify-document"
|
||||
element={<Lazy Page={VerifyDocument} />}
|
||||
/>
|
||||
<Route
|
||||
path="/preferences"
|
||||
element={<Lazy Page={Preferences} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/success" element={<DocSuccessPage />} />
|
||||
<Route path="/emailbuilder" element={<EmailBuilder />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -1,300 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Parse from "parse";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { emailRegex } from "../constant/const";
|
||||
|
||||
const AddSigner = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [addYourself, setAddYourself] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [isUserExist, setIsUserExist] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserExist();
|
||||
}, []);
|
||||
// Load user details from localStorage when the component mounts
|
||||
useEffect(() => {
|
||||
const savedUserDetails = JSON.parse(
|
||||
localStorage.getItem("UserInformation")
|
||||
);
|
||||
if (savedUserDetails && addYourself) {
|
||||
setName(savedUserDetails.name);
|
||||
setPhone(savedUserDetails?.phone || "");
|
||||
setEmail(savedUserDetails.email);
|
||||
}
|
||||
}, [addYourself]);
|
||||
|
||||
const checkUserExist = async () => {
|
||||
const user = Parse.User.current();
|
||||
try {
|
||||
const query = new Parse.Query("contracts_Contactbook");
|
||||
query.equalTo("CreatedBy", user);
|
||||
query.notEqualTo("IsDeleted", true);
|
||||
query.equalTo("Email", user.getEmail());
|
||||
const res = await query.first();
|
||||
// console.log(res);
|
||||
if (!res) {
|
||||
setIsUserExist(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!emailRegex.test(email)) {
|
||||
alert("Please enter a valid email address.");
|
||||
} else {
|
||||
setIsLoader(true);
|
||||
if (localStorage.getItem("TenantId")) {
|
||||
try {
|
||||
const user = Parse.User.current();
|
||||
const query = new Parse.Query("contracts_Contactbook");
|
||||
query.equalTo("CreatedBy", user);
|
||||
query.notEqualTo("IsDeleted", true);
|
||||
query.equalTo("Email", email);
|
||||
const res = await query.first();
|
||||
// console.log(res);
|
||||
if (!res) {
|
||||
const contactQuery = new Parse.Object("contracts_Contactbook");
|
||||
contactQuery.set("Name", name);
|
||||
if (phone) {
|
||||
contactQuery.set("Phone", phone);
|
||||
}
|
||||
contactQuery.set("Email", email);
|
||||
contactQuery.set("UserRole", "contracts_Guest");
|
||||
|
||||
contactQuery.set("TenantId", {
|
||||
__type: "Pointer",
|
||||
className: "partners_Tenant",
|
||||
objectId: localStorage.getItem("TenantId")
|
||||
});
|
||||
|
||||
try {
|
||||
const _users = Parse.Object.extend("User");
|
||||
const _user = new _users();
|
||||
_user.set("name", name);
|
||||
_user.set("username", email);
|
||||
_user.set("email", email);
|
||||
_user.set("password", email);
|
||||
if (phone) {
|
||||
_user.set("phone", phone);
|
||||
}
|
||||
const user = await _user.save();
|
||||
if (user) {
|
||||
const currentUser = Parse.User.current();
|
||||
contactQuery.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
contactQuery.set("UserId", user);
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
contactQuery.setACL(acl);
|
||||
|
||||
const res = await contactQuery.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.details) {
|
||||
props.details({
|
||||
value: parseData[props.valueKey],
|
||||
label: parseData[props.displayKey],
|
||||
email: parseData?.Email
|
||||
});
|
||||
}
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
|
||||
setIsLoader(false);
|
||||
// Reset the form fields
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err ", err);
|
||||
if (err.code === 202) {
|
||||
const params = { email: email };
|
||||
const userRes = await Parse.Cloud.run("getUserId", params);
|
||||
const currentUser = Parse.User.current();
|
||||
contactQuery.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
contactQuery.set("UserId", {
|
||||
__type: "Pointer",
|
||||
className: "_User",
|
||||
objectId: userRes.id
|
||||
});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
contactQuery.setACL(acl);
|
||||
const res = await contactQuery.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.details) {
|
||||
props.details({
|
||||
value: parseData[props.valueKey],
|
||||
label: parseData[props.displayKey],
|
||||
email: parseData?.Email
|
||||
});
|
||||
}
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
setIsLoader(false);
|
||||
// Reset the form fields
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(t("add-signer-alert"));
|
||||
setIsLoader(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err in fetch contact", err);
|
||||
setIsLoader(false);
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
alert(t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleAddYourselfChange = () => {
|
||||
if (addYourself) {
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
} else {
|
||||
setAddYourself(true);
|
||||
}
|
||||
};
|
||||
const handleReset = () => {
|
||||
setAddYourself(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-[20px]">
|
||||
{isLoader && (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full mx-auto p-2 text-base-content">
|
||||
<p className="mb-3 text-xs text-gray-500">{t("add-signer-note")}</p>
|
||||
{isUserExist && (
|
||||
<div className="mb-3 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="addYourself"
|
||||
checked={addYourself}
|
||||
onChange={handleAddYourselfChange}
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
/>
|
||||
<label htmlFor="addYourself" className="ml-2 mb-0">
|
||||
{t("add-yourself")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="name" className="block text-xs font-semibold">
|
||||
{t("name")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
disabled={addYourself}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="block text-xs font-semibold">
|
||||
{t("email")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) =>
|
||||
setEmail(e.target.value?.toLowerCase()?.replace(/\s/g, ""))
|
||||
}
|
||||
required
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="phone" className="block text-xs font-semibold">
|
||||
{t("phone")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("phone-optional")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-x-2 justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("submit")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReset()}
|
||||
className="op-btn op-btn-secondary"
|
||||
>
|
||||
{t("reset")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSigner;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Parse from "parse";
|
||||
import Title from "./Title";
|
||||
import Loader from "../primitives/Loader";
|
||||
import {
|
||||
copytoData,
|
||||
@@ -9,7 +8,11 @@ import {
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../constant/const";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useTranslation
|
||||
} from "react-i18next";
|
||||
import { withSessionValidation } from "../utils";
|
||||
|
||||
function generatePassword(length) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
@@ -66,181 +69,70 @@ const AddUser = (props) => {
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = withSessionValidation(async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!emailRegex.test(formdata.email)) {
|
||||
alert("Please enter a valid email address.");
|
||||
alert(t("valid-email-alert"));
|
||||
} else {
|
||||
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
setIsFormLoader(true);
|
||||
const res = await checkUserExist();
|
||||
if (res) {
|
||||
props.setIsAlert({ type: "danger", msg: t("user-already-exist") });
|
||||
props.showAlert("danger", t("user-already-exist"));
|
||||
setIsFormLoader(false);
|
||||
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1000);
|
||||
} else {
|
||||
try {
|
||||
const extUser = new Parse.Object("contracts_Users");
|
||||
extUser.set("Name", formdata.name);
|
||||
if (formdata.phone) {
|
||||
extUser.set("Phone", formdata.phone);
|
||||
}
|
||||
extUser.set("Email", formdata.email);
|
||||
extUser.set("UserRole", `contracts_${formdata.role}`);
|
||||
if (formdata?.team) {
|
||||
extUser.set("TeamIds", [
|
||||
{
|
||||
__type: "Pointer",
|
||||
className: "contracts_Teams",
|
||||
objectId: formdata.team
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (localUser && localUser.OrganizationId) {
|
||||
extUser.set("OrganizationId", {
|
||||
__type: "Pointer",
|
||||
className: "contracts_Organizations",
|
||||
objectId: localUser.OrganizationId.objectId
|
||||
});
|
||||
}
|
||||
if (localUser && localUser.Company) {
|
||||
extUser.set("Company", localUser.Company);
|
||||
}
|
||||
|
||||
if (localStorage.getItem("TenantId")) {
|
||||
extUser.set("TenantId", {
|
||||
__type: "Pointer",
|
||||
className: "partners_Tenant",
|
||||
objectId: localStorage.getItem("TenantId")
|
||||
});
|
||||
}
|
||||
if (localStorage.getItem("TenantId")) {
|
||||
const timezone = usertimezone;
|
||||
if (timezone) {
|
||||
extUser.set("Timezone", timezone);
|
||||
}
|
||||
try {
|
||||
const _users = Parse.Object.extend("User");
|
||||
const _user = new _users();
|
||||
_user.set("name", formdata.name);
|
||||
_user.set("username", formdata.email);
|
||||
_user.set("email", formdata.email);
|
||||
_user.set("password", formdata.password);
|
||||
if (formdata.phone) {
|
||||
_user.set("phone", formdata.phone);
|
||||
const params = {
|
||||
name: formdata.name,
|
||||
email: formdata.email,
|
||||
phone: formdata.phone,
|
||||
password: formdata.password,
|
||||
role: formdata.role,
|
||||
team: formdata.team,
|
||||
timezone: timezone,
|
||||
tenantId: localStorage.getItem("TenantId"),
|
||||
organization: {
|
||||
objectId: localUser?.OrganizationId?.objectId,
|
||||
company: localUser?.Company
|
||||
},
|
||||
};
|
||||
const res = await Parse.Cloud.run("adduser", params);
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
|
||||
const user = await _user.save();
|
||||
if (user) {
|
||||
const currentUser = Parse.User.current();
|
||||
extUser.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
extUser.set("UserId", user);
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
extUser.setACL(acl);
|
||||
|
||||
const res = await extUser.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
if (props.handleUserData) {
|
||||
if (formdata?.team) {
|
||||
const team = teamList.find((x) => x.objectId === formdata.team);
|
||||
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||
y.objectId === team.objectId ? team : y
|
||||
);
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
if (formdata?.team) {
|
||||
const team = teamList.find(
|
||||
(x) => x.objectId === formdata.team
|
||||
);
|
||||
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||
y.objectId === team.objectId ? team : y
|
||||
);
|
||||
}
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
|
||||
setIsFormLoader(false);
|
||||
setFormdata({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
team: "",
|
||||
role: ""
|
||||
});
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
setIsFormLoader(false);
|
||||
setFormdata({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
team: "",
|
||||
role: ""
|
||||
});
|
||||
props.showAlert("success", t("user-created-successfully"));
|
||||
} catch (err) {
|
||||
console.log("err ", err);
|
||||
if (err.code === 202) {
|
||||
const params = { email: formdata.email };
|
||||
const userRes = await Parse.Cloud.run("getUserId", params);
|
||||
const currentUser = Parse.User.current();
|
||||
extUser.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
extUser.set("UserId", {
|
||||
__type: "Pointer",
|
||||
className: "_User",
|
||||
objectId: userRes.id
|
||||
});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
extUser.setACL(acl);
|
||||
const res = await extUser.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
if (formdata?.team) {
|
||||
const team = teamList.find(
|
||||
(x) => x.objectId === formdata.team
|
||||
);
|
||||
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||
y.objectId === team.objectId ? team : y
|
||||
);
|
||||
}
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
setIsFormLoader(false);
|
||||
setFormdata({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
team: "",
|
||||
role: ""
|
||||
});
|
||||
}
|
||||
console.log("err", err);
|
||||
setIsFormLoader(false);
|
||||
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
setIsFormLoader(false);
|
||||
props.setIsAlert({
|
||||
type: "danger",
|
||||
msg: t("something-went-wrong-mssg")
|
||||
});
|
||||
} finally {
|
||||
setTimeout(
|
||||
() => props.setIsAlert({ type: "success", msg: "" }),
|
||||
1500
|
||||
);
|
||||
} else {
|
||||
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleReset = () => {
|
||||
@@ -259,12 +151,10 @@ const AddUser = (props) => {
|
||||
|
||||
const copytoclipboard = (text) => {
|
||||
copytoData(text);
|
||||
props.setIsAlert({ type: "success", msg: t("copied") });
|
||||
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1500); // Reset copied state after 1.5 seconds
|
||||
props.showAlert("success", t("copied"));
|
||||
};
|
||||
return (
|
||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
||||
<Title title={t("add-user")} />
|
||||
{isFormLoader && (
|
||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
||||
<Loader />
|
||||
@@ -275,7 +165,7 @@ const AddUser = (props) => {
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("name")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
@@ -291,12 +181,13 @@ const AddUser = (props) => {
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("email")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
@@ -312,10 +203,11 @@ const AddUser = (props) => {
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs text-gray-700 font-semibold">
|
||||
<label className="block text-xs font-semibold">
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
||||
@@ -326,16 +218,15 @@ const AddUser = (props) => {
|
||||
></i>
|
||||
</div>
|
||||
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
||||
{t("password-generateed")}
|
||||
{t("password-generated")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("phone")}
|
||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -349,7 +240,7 @@ const AddUser = (props) => {
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("Role")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
@@ -1,287 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import SuggestionInput from "./shared/fields/SuggestionInput";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../constant/const";
|
||||
const BulkSendUi = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [forms, setForms] = useState([]);
|
||||
const formRef = useRef(null);
|
||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//function to check atleast one signature field exist
|
||||
const signatureExist = async () => {
|
||||
setIsDisableBulkSend(false);
|
||||
const getPlaceholder = props?.Placeholders;
|
||||
const checkIsSignatureExistt = getPlaceholder?.every((placeholderObj) =>
|
||||
placeholderObj?.placeHolder?.some((holder) =>
|
||||
holder?.pos?.some((posItem) => posItem?.type === "signature")
|
||||
)
|
||||
);
|
||||
setIsSignatureExist(checkIsSignatureExistt);
|
||||
setIsLoader(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (scrollOnNextUpdate && formRef.current) {
|
||||
formRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
setScrollOnNextUpdate(false);
|
||||
}
|
||||
}, [forms, scrollOnNextUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const handleInputChange = (index, signer, fieldIndex) => {
|
||||
const newForms = [...forms];
|
||||
newForms[index].fields[fieldIndex].email = signer?.Email
|
||||
? signer?.Email
|
||||
: signer || "";
|
||||
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
|
||||
const handleRemoveForm = (index) => {
|
||||
const updatedForms = forms.filter((_, i) => i !== index);
|
||||
setForms(updatedForms);
|
||||
};
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const item of data) {
|
||||
for (const field of item.fields) {
|
||||
if (!emailRegex.test(field.email)) {
|
||||
alert(`Invalid email found: ${field.email}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSubmit(true);
|
||||
if (validateEmails(forms)) {
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.Placeholders];
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
// Loop through each form
|
||||
forms.forEach((form) => {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: field.email,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
Signers: props.item.Signers
|
||||
? [...props.item.Signers, ...existSigner]
|
||||
: [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
SignatureType: props.signatureType
|
||||
});
|
||||
}
|
||||
});
|
||||
await batchQuery(Documents);
|
||||
} else {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const token =
|
||||
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
|
||||
const functionsUrl = `${localStorage.getItem(
|
||||
"baseUrl"
|
||||
)}functions/batchdocuments`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
...token,
|
||||
};
|
||||
const params = { Documents: JSON.stringify(Documents) };
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
// console.log("res ", res);
|
||||
if (res.data && res.data.result) {
|
||||
props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
props.handleClose("error", 0);
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isSubmit && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{!isDisableBulkSend ? (
|
||||
<>
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
<>
|
||||
{props.Placeholders?.some((x) => !x.signerObjId) ? (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-y-auto">
|
||||
{forms?.map((form, index) => (
|
||||
<div
|
||||
key={form.Id}
|
||||
className="p-3 op-card border-[1px] border-gray-400 mt-3 mx-4 mb-4 bg-base-200 text-base-content grid grid-cols-1 md:grid-cols-2 gap-2 relative"
|
||||
>
|
||||
{form?.fields?.map((field, fieldIndex) => (
|
||||
<div
|
||||
className="flex flex-col"
|
||||
key={field.fieldId}
|
||||
>
|
||||
<label>{field.label}</label>
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.value}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(
|
||||
index,
|
||||
signer,
|
||||
fieldIndex
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{forms?.length > 1 && (
|
||||
<button
|
||||
onClick={() => handleRemoveForm(index)}
|
||||
className="absolute right-3 top-1 text-[red] border-[1px] border-[red] rounded-lg w-[1.7rem] h-[1.7rem]"
|
||||
>
|
||||
<i className="fa-light fa-trash"></i>
|
||||
</button>
|
||||
)}
|
||||
<div ref={formRef}></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col mx-4 mb-4 gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-accent focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>{" "}
|
||||
<span>{t("send")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-1")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-2")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-3")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import ModalUi from "../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ColumnSelector = ({
|
||||
isOpen,
|
||||
allColumns = [],
|
||||
visibleColumns = [],
|
||||
columnLabels = {},
|
||||
onApply,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState(visibleColumns);
|
||||
const [names, setNames] = useState(columnLabels);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(visibleColumns);
|
||||
setNames(columnLabels);
|
||||
}, [visibleColumns, columnLabels]);
|
||||
|
||||
const handleChange = (col) => {
|
||||
setSelected((prev) =>
|
||||
prev.includes(col) ? prev.filter((c) => c !== col) : [...prev, col]
|
||||
);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
onApply && onApply(selected, names);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi isOpen={isOpen} title={t("select-columns")} handleClose={onClose}>
|
||||
<div className="p-[20px] flex flex-col gap-2">
|
||||
{allColumns.map((col, i) => (
|
||||
<div key={col} className="flex justify-between items-center gap-2">
|
||||
<span className="flex justify-center items-center h-full">
|
||||
<input
|
||||
id={col + "_" + i}
|
||||
type="checkbox"
|
||||
checked={selected.includes(col)}
|
||||
onChange={() => handleChange(col)}
|
||||
className="mb-0 cursor-pointer"
|
||||
/>
|
||||
<span className="whitespace-nowrap ml-1">
|
||||
{t(`report-heading.${col}`, { defaultValue: col })}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-start mt-2">
|
||||
<button onClick={handleApply} className="op-btn op-btn-primary">
|
||||
{t("apply")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnSelector;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DateFormat = ({ selectDate, dateFormatList, handleChangeFormat }) => {
|
||||
const selectedFormatIndex = dateFormatList?.findIndex(
|
||||
(item) => item.format === selectDate?.format
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:items-center md:flex-row gap-y-3 my-2">
|
||||
<span className="capitalize">{t("format")}: </span>
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs md:ml-2"
|
||||
value={selectedFormatIndex >= 0 ? selectedFormatIndex : ""}
|
||||
onChange={(e) => handleChangeFormat(e)}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{t("select-date-format")}
|
||||
</option>
|
||||
{dateFormatList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" value={ind} key={ind}>
|
||||
{data?.date ? data?.date : "nodata"}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span className="text-xs text-gray-400 ml-1 uppercase">
|
||||
{selectDate.format}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateFormat;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { forwardRef } from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import {
|
||||
getDefaultDate,
|
||||
getMonth,
|
||||
getYear,
|
||||
months,
|
||||
years
|
||||
} from "../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DatePicker = ({
|
||||
selectDate,
|
||||
format,
|
||||
minDate,
|
||||
maxDate,
|
||||
onChange,
|
||||
handleClear,
|
||||
label,
|
||||
showLabel = true,
|
||||
showClear = true,
|
||||
dateClassName = ""
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const CustomInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<div
|
||||
className="w-full border-gray-400 rounded-[50px] border-[1px] px-3 text-xs py-2 focus:outline-none hover:border-base-content flex items-center justify-between"
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<span className={`${dateClassName} truncate`}>{value}</span>
|
||||
<i className={`fa-light fa-calendar`}></i>
|
||||
</div>
|
||||
));
|
||||
CustomInput.displayName = "CustomInput";
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLabel && (
|
||||
<span className="flex-shrink-0">{label || t("default-date")}: </span>
|
||||
)}
|
||||
<ReactDatePicker
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<div className="flex justify-start md:ml-2">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) => changeYear(value)}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
wrapperClassName="w-full"
|
||||
closeOnScroll={true}
|
||||
selected={getDefaultDate(selectDate?.date, selectDate?.format)}
|
||||
popperPlacement="top-end"
|
||||
customInput={<CustomInput />}
|
||||
onChange={(date) => onChange(date)}
|
||||
dateFormat={
|
||||
selectDate ? selectDate?.format : format ? format : "MM/dd/yyyy"
|
||||
}
|
||||
portalId="root-portal"
|
||||
/>
|
||||
{showClear && handleClear && (
|
||||
<span
|
||||
onClick={() => handleClear()}
|
||||
className="underline text-blue-500 cursor-pointer ml-2"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import {
|
||||
DndProvider,
|
||||
MouseTransition
|
||||
} from "react-dnd-multi-backend";
|
||||
import LazyPage from "../primitives/LazyPage";
|
||||
import { GuidelinesProvider } from "../context/GuidelinesContext";
|
||||
|
||||
const HTML5toTouch = {
|
||||
backends: [
|
||||
{
|
||||
id: "html5",
|
||||
backend: HTML5Backend,
|
||||
transition: MouseTransition
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default function DragProvider({ Page, lazy = false }) {
|
||||
return (
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
<GuidelinesProvider>
|
||||
{lazy ? <LazyPage Page={Page} /> : <Page />}
|
||||
</GuidelinesProvider>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import juice from "juice";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
const EmailBodyEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
smallscreen = false,
|
||||
bodyName,
|
||||
isReset = false,
|
||||
isTemplateLoaded
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [inputHtml, setInputHtml] = useState("");
|
||||
const [cleanPreview, setCleanPreview] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
initProcessContent();
|
||||
}, [isTemplateLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReset) {
|
||||
initProcessContent(value);
|
||||
}
|
||||
}, [isReset]);
|
||||
|
||||
const initProcessContent = () => {
|
||||
// 1. Sanitize immediately to strip scripts/malicious tags
|
||||
const sanitized = DOMPurify.sanitize(value, {
|
||||
USE_PROFILES: { html: true }, // Ensures basic HTML structure
|
||||
ADD_ATTR: ["target"] // Allow links to open in new tabs
|
||||
});
|
||||
|
||||
// 2. Inline CSS for email compatibility
|
||||
// Note: Juice is fast, but for huge files, you might debounce this
|
||||
try {
|
||||
const inlined = juice(sanitized);
|
||||
setInputHtml(inlined);
|
||||
onChange?.(inlined);
|
||||
setCleanPreview(inlined);
|
||||
} catch (err) {
|
||||
onChange?.(sanitized);
|
||||
setInputHtml(sanitized); // Fallback if juice fails
|
||||
setCleanPreview(sanitized);
|
||||
}
|
||||
};
|
||||
|
||||
const processContent = (value) => {
|
||||
// 1. Sanitize immediately to strip scripts/malicious tags
|
||||
const sanitized = DOMPurify.sanitize(value, {
|
||||
USE_PROFILES: { html: true }, // Ensures basic HTML structure
|
||||
ADD_ATTR: ["target"] // Allow links to open in new tabs
|
||||
});
|
||||
|
||||
// 2. Inline CSS for email compatibility
|
||||
// Note: Juice is fast, but for huge files, you might debounce this
|
||||
try {
|
||||
const inlined = juice(sanitized);
|
||||
setCleanPreview(inlined);
|
||||
onChange?.(inlined);
|
||||
} catch (err) {
|
||||
setCleanPreview(sanitized); // Fallback if juice fails
|
||||
onChange?.(sanitized);
|
||||
}
|
||||
};
|
||||
const handleChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setInputHtml(value);
|
||||
processContent(value);
|
||||
};
|
||||
|
||||
const screen = smallscreen ? "flex-col" : "flex-col md:flex-row ";
|
||||
const template =
|
||||
bodyName === "request"
|
||||
? "#sample/requestemail"
|
||||
: bodyName === "completion"
|
||||
? "#sample/completionemail"
|
||||
: "#";
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm ">
|
||||
<Trans i18nKey={"open-email-builder"}>
|
||||
{"You can create email template using "}
|
||||
<a
|
||||
href={`/emailbuilder${template}`}
|
||||
target="_blank"
|
||||
referrerPolicy="no-referrer"
|
||||
className="op-link op-link-primary font-medium"
|
||||
>
|
||||
email builder
|
||||
</a>
|
||||
{" platform and copy html code."}
|
||||
</Trans>
|
||||
</p>
|
||||
<div className={`flex ${screen} gap-5`}>
|
||||
{/* Editor Pane */}
|
||||
<div className="flex flex-1 flex-col mt-2">
|
||||
<label>{t("paste-html-here")}:</label>
|
||||
<div className="flex-1 mt-1 p-3 text-xs op-textarea op-textarea-bordered min-h-[70vh]">
|
||||
<textarea
|
||||
className="w-full min-h-[70vh] focus:outline-none"
|
||||
value={inputHtml}
|
||||
onChange={(e) => handleChange(e)}
|
||||
placeholder="<html><body><h1>Hello!</h1></body></html>"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Preview Pane */}
|
||||
<div className="flex flex-1 flex-col mt-2">
|
||||
<label>{t("preview")}</label>
|
||||
<div className="flex-1 mt-1 bg-white border-[1px] op-textarea border-[#ccc] min-h-[70vh]">
|
||||
{cleanPreview && (
|
||||
<iframe
|
||||
title="Safe Preview"
|
||||
srcDoc={cleanPreview}
|
||||
// SECURITY: sandbox prevents scripts from running even if they slip through
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox"
|
||||
className="w-full min-h-[70vh]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailBodyEditor;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import dp from "../assets/images/dp.png";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
import { useNavigate } from "react-router";
|
||||
import Parse from "parse";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
@@ -11,26 +12,45 @@ import {
|
||||
} from "../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { appInfo } from "../constant/appinfo";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { toggleSidebar } from "../redux/reducers/sidebarReducer";
|
||||
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||
|
||||
const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { width } = useWindowSize();
|
||||
const dispatch = useDispatch();
|
||||
const username = localStorage.getItem("username") || "";
|
||||
const image = localStorage.getItem("profileImg") || dp;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [applogo, setAppLogo] = useState("");
|
||||
const [isDarkTheme, setIsDarkTheme] = useState();
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen(!isOpen);
|
||||
if (width <= 768) {
|
||||
setIsMenu(false);
|
||||
closeSidebar();
|
||||
};
|
||||
const closeSidebar = () => {
|
||||
if (width && width <= 768) {
|
||||
dispatch(toggleSidebar(false));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initializeHead();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
closeSidebar();
|
||||
}, [width]);
|
||||
|
||||
const showSidebar = () => {
|
||||
dispatch(toggleSidebar());
|
||||
};
|
||||
|
||||
|
||||
async function initializeHead() {
|
||||
const applogo = await getAppLogo();
|
||||
if (applogo?.logo) {
|
||||
@@ -40,13 +60,15 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
setAppLogo(logo);
|
||||
}
|
||||
}
|
||||
|
||||
const closeDropdown = async () => {
|
||||
const handleLogout = async () => {
|
||||
setIsOpen(false);
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
await Parse.User.logOut();
|
||||
} catch (err) {
|
||||
console.log("Err while logging out", err);
|
||||
} finally {
|
||||
dispatch(sessionStatus(true));
|
||||
}
|
||||
let appdata = localStorage.getItem("userSettings");
|
||||
let applogo = localStorage.getItem("appLogo");
|
||||
@@ -54,6 +76,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
let PageLanding = localStorage.getItem("PageLanding");
|
||||
let baseUrl = localStorage.getItem("baseUrl");
|
||||
let appid = localStorage.getItem("parseAppId");
|
||||
let favicon = localStorage.getItem("favicon");
|
||||
|
||||
localStorage.clear();
|
||||
saveLanguageInLocal(i18n);
|
||||
@@ -63,7 +86,8 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
localStorage.setItem("userSettings", appdata);
|
||||
localStorage.setItem("baseUrl", baseUrl);
|
||||
localStorage.setItem("parseAppId", appid);
|
||||
|
||||
localStorage.setItem("favicon", favicon);
|
||||
setIsLoggingOut(false);
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
@@ -83,9 +107,30 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const updateThemeStatus = () => {
|
||||
const isDarkTheme =
|
||||
document.documentElement.getAttribute("data-theme") === "opensigndark";
|
||||
setIsDarkTheme(isDarkTheme);
|
||||
};
|
||||
updateThemeStatus();
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
updateThemeStatus();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"]
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="op-navbar bg-base-100 shadow">
|
||||
<>
|
||||
<div className="op-navbar bg-base-100 shadow touch-none">
|
||||
<div className="flex-none">
|
||||
<button
|
||||
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
|
||||
@@ -95,11 +140,18 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 ml-2">
|
||||
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
|
||||
<div
|
||||
onClick={() => navigate("/dashboard/35KBoSgoAK")}
|
||||
className="h-[25px] md:h-[40px] w-auto overflow-hidden cursor-pointer"
|
||||
>
|
||||
{applogo && (
|
||||
<img
|
||||
className="object-contain h-full w-auto"
|
||||
src={applogo}
|
||||
src={
|
||||
isDarkTheme
|
||||
? "/static/js/assets/images/logo-dark.png"
|
||||
: applogo
|
||||
}
|
||||
alt="logo"
|
||||
/>
|
||||
)}
|
||||
@@ -145,7 +197,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-52 ${
|
||||
className={`mt-4 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
|
||||
isOpen ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
@@ -170,20 +222,41 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
<i className="fa-light fa-user"></i> {t("profile")}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/changepassword");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-lock"></i>{" "}
|
||||
{t("change-password")}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/changepassword");
|
||||
navigate("/verify-document");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-lock"></i>{" "}
|
||||
{t("change-password")}
|
||||
<i className="fa-light fa-check-square"></i>{" "}
|
||||
{t("verify-document")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
<i className="fa-light fa-moon"></i>
|
||||
{t("dark-mode")}
|
||||
<span className="text-[10px] font-semibold bg-base-300 text-base-content px-1 rounded-md">
|
||||
BETA
|
||||
</span>
|
||||
<ThemeToggle />
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li onClick={closeDropdown}>
|
||||
<li onClick={handleLogout}>
|
||||
<span>
|
||||
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
|
||||
{t("log-out")}
|
||||
@@ -193,7 +266,7 @@ const Header = ({ showSidebar, setIsMenu, isConsole }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import { Document, Page } from "react-pdf";
|
||||
import { Stage, Layer, Rect, Text } from "react-konva";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const RenderDebugPdf = (props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<div className="sticky top-0 p-[10px] z-10 bg-white border-[1px] border-[gray] my-[5px]">
|
||||
@@ -12,10 +14,9 @@ const RenderDebugPdf = (props) => {
|
||||
onMouseMove={props.handleMouseMoveDiv}
|
||||
>
|
||||
<Document
|
||||
onLoadError={() => {
|
||||
props.setPdfLoadFail(false);
|
||||
}}
|
||||
loading={"Loading Document.."}
|
||||
onLoadError={() => props.setPdfLoadFail(false)}
|
||||
loading={t("loading-doc")}
|
||||
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
|
||||
onLoadSuccess={props.pageDetails}
|
||||
ref={props.pdfRef}
|
||||
file={props.pdfUrl}
|
||||
@@ -26,7 +26,7 @@ function RotateAlert(props) {
|
||||
props.setShowRotateAlert({ status: false, degree: 0 })
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost shadow-md"
|
||||
className="op-btn op-btn-ghost text-base-content shadow-md"
|
||||
>
|
||||
{t("no")}
|
||||
</button>
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme === "dark") {
|
||||
setIsDark(true);
|
||||
document.documentElement.setAttribute("data-theme", "opensigndark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "opensigncss");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = () => {
|
||||
const newTheme = !isDark;
|
||||
setIsDark(newTheme);
|
||||
if (newTheme) {
|
||||
document.documentElement.setAttribute("data-theme", "opensigndark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "opensigncss");
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
id="dark-mode-toggle"
|
||||
type="checkbox"
|
||||
className="op-toggle checked:[--tglbg:#3368ff] transition-all checked:bg-white"
|
||||
checked={isDark}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
function Title({ title, drive }) {
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{drive ? title : `${title} - ${appName}`}</title>
|
||||
<meta name="description" content={`${title} - ${appName}`} />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href={localStorage.getItem("fev_Icon")}
|
||||
sizes="40x40"
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
||||
export default Title;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useLocation, matchPath } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { useManifestUrl } from "../hook/useManifestUrl";
|
||||
|
||||
const TITLE_MAP = {
|
||||
"/": "login",
|
||||
// Homelayout
|
||||
"/dashboard/35KBoSgoAK": "Dashboard",
|
||||
"/form/sHAnZphf69": "Sign Yourself",
|
||||
"/form/8mZzFxbG1z": "Request Signatures",
|
||||
"/form/template": "New Template",
|
||||
"/report/6TeaPr321t": "Templates",
|
||||
"/report/4Hhwbp482K": "Need your sign",
|
||||
"/report/1MwEuxLEkF": "In Progress",
|
||||
"/report/kQUoW4hUXz": "Completed",
|
||||
"/report/ByHuevtCFY": "Drafts",
|
||||
"/report/UPr2Fm5WY3": "Declined",
|
||||
"/report/zNqBHXHsYH": "Expired",
|
||||
"/report/contacts": "Contactbook",
|
||||
"/drive": "Drive",
|
||||
"/managesign": "My Signature",
|
||||
"/preferences": "Preferences",
|
||||
"/users": "Users",
|
||||
"/profile": "profile",
|
||||
"/changepassword": "change-password",
|
||||
"/verify-document": "verify-document",
|
||||
|
||||
"/signaturePdf/:docId": "Sign Yourself",
|
||||
"/placeHolderSign/:docId": "Request Signatures",
|
||||
"/template/:templateId": "New Template",
|
||||
"/recipientSignPdf/:docId": "Request Signatures",
|
||||
"/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||
"/load/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||
|
||||
// alone
|
||||
"/debugpdf": "Debug Pdf",
|
||||
"/forgetpassword": "forgot-password",
|
||||
"/success": "success",
|
||||
"/addadmin": "add-admin",
|
||||
"/upgrade-2.1": "add-admin",
|
||||
"/draftDocument": "New Document",
|
||||
"/login/:base64url": "Request Signatures",
|
||||
|
||||
};
|
||||
|
||||
function resolveTitle(pathname, override) {
|
||||
if (override) return override;
|
||||
for (let [pattern, label] of Object.entries(TITLE_MAP)) {
|
||||
if (matchPath({ path: pattern, end: true }, pathname)) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export default function Title() {
|
||||
const { pathname, state } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const logo = useMemo(() => localStorage.getItem("favicon"), []);
|
||||
const prefix = useMemo(
|
||||
() => resolveTitle(pathname, state?.title),
|
||||
[pathname, state?.title]
|
||||
);
|
||||
const title = useMemo(
|
||||
() => (prefix ? `${t(prefix)} - ${appName}` : appName),
|
||||
[t, prefix, appName]
|
||||
);
|
||||
const manifestUrl = useManifestUrl(appName, logo);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={title} />
|
||||
{logo && <link rel="icon" type="image/png" href={logo} />}
|
||||
<link rel="manifest" href={manifestUrl} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo
|
||||
} from "react";
|
||||
import axios from "axios";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../../constant/const";
|
||||
import {
|
||||
getSignedUrl,
|
||||
isBase64,
|
||||
getBase64FromUrl,
|
||||
generateId,
|
||||
} from "../../constant/Utils";
|
||||
import {
|
||||
handleEmbedPrefillToDoc,
|
||||
isWidgetResponseCompatible,
|
||||
formatCSVDate,
|
||||
normalizeKey,
|
||||
loadPdfOnce,
|
||||
} from "../../utils";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
setBulkLoader,
|
||||
} from "../../redux/reducers/widgetSlice";
|
||||
import Table from "./components/Table";
|
||||
import PrefillWidgets from "./components/PrefillWidgets";
|
||||
import WizardHeader from "./components/WizardHeader";
|
||||
import ResponseTab from "./components/ResponseTab";
|
||||
import { steps } from "../../json/BulkSendSteps";
|
||||
|
||||
const EXCLUDED_PREFILL_TYPES = new Set(["image", "draw"]);
|
||||
const EXCLUDED_WIDGET_TYPES = new Set([
|
||||
"image",
|
||||
"signature",
|
||||
"stamp",
|
||||
"initials"
|
||||
]);
|
||||
const ALL_EXCLUDED_TYPES = new Set([
|
||||
...EXCLUDED_PREFILL_TYPES,
|
||||
...EXCLUDED_WIDGET_TYPES
|
||||
]);
|
||||
|
||||
const BulkSendUi = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const dispatch = useDispatch();
|
||||
const { isBulkLoader } = useSelector((state) => state.widget);
|
||||
const [forms, setForms] = useState([]);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
const [emails, setEmails] = useState([]);
|
||||
const [isVacantRoles, setIsVacantRoles] = useState(false);
|
||||
const [fields, setFields] = useState([]);
|
||||
const [headers, setHeaders] = useState([]);
|
||||
const [prefillWidgets, setPrefillWidgets] = useState([]);
|
||||
const [step, setStep] = useState(0);
|
||||
const [stepsList, setStepsList] = useState(steps(t));
|
||||
const [stepKey, setStepKey] = useState(steps(t)?.[0]?.key || "");
|
||||
const [message, setMessage] = useState({ status: "", message: "" });
|
||||
|
||||
const tokenHeader = useMemo(() => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId")
|
||||
};
|
||||
return {
|
||||
...headers,
|
||||
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
||||
};
|
||||
}, [props?.jwttoken]);
|
||||
|
||||
// Initialize form state (fields, widgets/responses, emails, headers, forms, signers, allowedForm) from template placeholders with existing values.
|
||||
const initializeWidgetsWithValues = async () => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
const roles = props?.Placeholders?.filter((p) => !p.signerObjId) || [];
|
||||
const emails = [];
|
||||
|
||||
const users = await Promise.all(
|
||||
roles?.map(async (role) => {
|
||||
const isPrefill = role.Role?.toLowerCase() === "prefill";
|
||||
const uniqueNames = new Set();
|
||||
const pages = role?.placeHolder ?? [];
|
||||
const widgets = await Promise.all(
|
||||
pages.flatMap((page) =>
|
||||
page.pos
|
||||
.filter((widget) => {
|
||||
const widgetName = widget?.options?.name;
|
||||
if (!widgetName) return false; //|| widget?.options?.isReadOnly
|
||||
// exclude certain widget types for non-prefill
|
||||
if (EXCLUDED_WIDGET_TYPES.has(widget.type) && !isPrefill)
|
||||
return false;
|
||||
if (widget?.options?.formula) return false;
|
||||
// unique by widget name
|
||||
if (uniqueNames.has(widgetName)) return false;
|
||||
uniqueNames.add(widgetName);
|
||||
return true;
|
||||
})
|
||||
.map(async (widget) => {
|
||||
let response = "";
|
||||
if (ALL_EXCLUDED_TYPES.has(widget.type)) {
|
||||
const url =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
if (url && !isBase64(url)) {
|
||||
const signedUrl = await getSignedUrl(
|
||||
url,
|
||||
"", // docId
|
||||
props.item.objectId, // templateId
|
||||
);
|
||||
|
||||
response = await getBase64FromUrl(signedUrl, true);
|
||||
} else {
|
||||
response = "";
|
||||
}
|
||||
} else {
|
||||
if (widget.type === "date") {
|
||||
const value =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
response = value ? formatCSVDate(widget, value) : "";
|
||||
} else {
|
||||
response =
|
||||
widget.options?.response ||
|
||||
widget.options?.defaultValue ||
|
||||
"";
|
||||
}
|
||||
}
|
||||
const label = `${role.Role}::${widget.options?.name}`;
|
||||
|
||||
return {
|
||||
...widget,
|
||||
options: { ...widget.options, response: response },
|
||||
label: label,
|
||||
response,
|
||||
pageNumber: page.pageNumber
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
const signerEmail =
|
||||
normalizeKey(role?.email) || role?.signerPtr?.Email;
|
||||
if (signerEmail) emails.push(signerEmail);
|
||||
|
||||
return {
|
||||
fieldId: role.Id,
|
||||
email: "",
|
||||
label: role.Role,
|
||||
signer: {},
|
||||
widgets: widgets
|
||||
};
|
||||
})
|
||||
);
|
||||
const prefills = users.find((r) => r?.label?.toLowerCase() === "prefill");
|
||||
const signers = users.filter(
|
||||
(r) => r?.label?.toLowerCase() !== "prefill"
|
||||
);
|
||||
setFields(signers);
|
||||
if (prefills && prefills?.widgets?.length) {
|
||||
setPrefillWidgets(prefills?.widgets);
|
||||
} else {
|
||||
setStepsList((s) => s.filter((step) => step.key !== "prefill"));
|
||||
setStepKey("recipients");
|
||||
setStep(0);
|
||||
}
|
||||
setEmails(emails);
|
||||
|
||||
// Build headers with required ordering:
|
||||
// 1) all role emails first
|
||||
// 2) then all widgets
|
||||
const emailHeaders = [];
|
||||
const widgetHeaders = [];
|
||||
for (const role of signers) {
|
||||
const isPrefill = role?.label?.toLowerCase() === "prefill";
|
||||
|
||||
if (!isPrefill) {
|
||||
emailHeaders.push({
|
||||
label: `${role.label}::Email`,
|
||||
role: role.label,
|
||||
type: "role",
|
||||
isRequired: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const widget of role?.widgets ?? []) {
|
||||
const isRequired =
|
||||
widget?.options?.status === "required" && isPrefill;
|
||||
|
||||
widgetHeaders.push({
|
||||
label: widget?.label,
|
||||
role: role?.label,
|
||||
type: widget?.type,
|
||||
isRequired
|
||||
});
|
||||
}
|
||||
}
|
||||
setHeaders([...emailHeaders, ...widgetHeaders]);
|
||||
setForms((prev) => [...prev, { Id: generateId(8), fields: signers }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const checkSignatureAndRoles = (placeholders = []) => {
|
||||
const filtered = placeholders.filter((x) => x?.Role !== "prefill");
|
||||
|
||||
const isSignExist =
|
||||
filtered?.length > 0 &&
|
||||
filtered.every((p) =>
|
||||
p?.placeHolder?.some((h) =>
|
||||
h?.pos?.some((x) => x?.type === "signature")
|
||||
)
|
||||
);
|
||||
setIsSignatureExist(isSignExist);
|
||||
setIsVacantRoles(filtered?.some((x) => !x.signerObjId));
|
||||
};
|
||||
|
||||
//function to check at least one signature field exist
|
||||
const signatureExist = async () => {
|
||||
setIsLoader(true);
|
||||
await initializeWidgetsWithValues();
|
||||
checkSignatureAndRoles(props?.Placeholders);
|
||||
setIsLoader(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (formIndex, signer, role) => {
|
||||
setForms((prev) => {
|
||||
const next = [...prev];
|
||||
const form = next[formIndex];
|
||||
if (!form) return prev;
|
||||
|
||||
const fields = [...(form.fields ?? [])];
|
||||
const fieldId = fields.findIndex((f) => f.label === role);
|
||||
if (fieldId < 0) return prev;
|
||||
|
||||
const email = signer?.Email ? signer.Email : signer || "";
|
||||
|
||||
fields[fieldId] = {
|
||||
...fields[fieldId],
|
||||
email: normalizeKey(email),
|
||||
signer: signer?.objectId ? signer : ""
|
||||
};
|
||||
|
||||
next[formIndex] = { ...form, fields };
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleWidgetDetails = (
|
||||
value = "",
|
||||
formIndex,
|
||||
roleIndex = 0,
|
||||
widgetIndex
|
||||
) => {
|
||||
setForms((prev) => {
|
||||
const next = [...prev];
|
||||
const form = next[formIndex];
|
||||
if (!form) return prev;
|
||||
|
||||
const fields = [...(form.fields ?? [])];
|
||||
const field = fields[roleIndex];
|
||||
if (!field) return prev;
|
||||
|
||||
const widgets = [...(field.widgets ?? [])];
|
||||
const w = widgets[widgetIndex];
|
||||
if (!w) return prev;
|
||||
|
||||
widgets[widgetIndex] = {
|
||||
...w,
|
||||
options: { ...w?.options, response: value },
|
||||
response: value
|
||||
};
|
||||
fields[roleIndex] = { ...field, widgets };
|
||||
next[formIndex] = { ...form, fields };
|
||||
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const [rowIndex, item] of data.entries()) {
|
||||
let email = "";
|
||||
const rowNumber = rowIndex + 1;
|
||||
const filterFields = item?.fields || [];
|
||||
for (const field of filterFields) {
|
||||
// skip prefill
|
||||
if (field?.label === "prefill") continue;
|
||||
const params = { email: field.email, row: rowNumber };
|
||||
if (!field.email) {
|
||||
alert(t("email-not-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else if (!emailRegex.test(field.email)) {
|
||||
alert(t("invalid-email-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found-in-row", { ...params }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// `updateWidgetValues` will update responses for all widget types
|
||||
const updateWidgetValues = (widgetValues = [], placeholders = []) => {
|
||||
if (!widgetValues?.length) return placeholders;
|
||||
const responseByName = new Map(
|
||||
widgetValues
|
||||
.map((w) => [w?.options?.name, w?.response])
|
||||
.filter(([name]) => !!name)
|
||||
);
|
||||
|
||||
return placeholders.map((ph) => {
|
||||
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
|
||||
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
|
||||
const name = widgetDetails?.options?.name;
|
||||
if (!name || !responseByName.has(name)) return widgetDetails;
|
||||
|
||||
const newResponse = responseByName?.get(name) || "";
|
||||
const isValidResponse = isWidgetResponseCompatible(
|
||||
widgetDetails,
|
||||
newResponse
|
||||
);
|
||||
|
||||
if (!isValidResponse) return widgetDetails;
|
||||
|
||||
return {
|
||||
...widgetDetails,
|
||||
options: {
|
||||
...widgetDetails.options,
|
||||
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
|
||||
response:
|
||||
newResponse ??
|
||||
widgetDetails.options?.response ??
|
||||
widgetDetails.options?.defaultValue
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { ...page, pos: updatedPos };
|
||||
});
|
||||
|
||||
return { ...ph, placeHolder: updatedPages };
|
||||
});
|
||||
};
|
||||
|
||||
// `removeWidgetValues` will clear out responses for draw/image/signature/stamp/initials widgets
|
||||
const removeWidgetValues = (widgetValues = [], placeholders = []) => {
|
||||
if (!widgetValues?.length) return placeholders;
|
||||
return placeholders.map((ph) => {
|
||||
const updatedPages = (ph?.placeHolder ?? []).map((page) => {
|
||||
const updatedPos = (page?.pos ?? []).map((widgetDetails) => {
|
||||
const isImageType = ALL_EXCLUDED_TYPES.has(widgetDetails?.type);
|
||||
return {
|
||||
...widgetDetails,
|
||||
options: {
|
||||
...widgetDetails.options,
|
||||
// update response; if you DON'T want undefined to overwrite, keep the ?? fallback
|
||||
response: isImageType ? "" : widgetDetails.options?.response,
|
||||
defaultValue: isImageType
|
||||
? ""
|
||||
: widgetDetails.options?.defaultValue
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { ...page, pos: updatedPos };
|
||||
});
|
||||
|
||||
return { ...ph, placeHolder: updatedPages };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(setBulkLoader(true));
|
||||
if (!forms.length) {
|
||||
alert(t("bulk-send-no-records"));
|
||||
dispatch(setBulkLoader(false));
|
||||
return;
|
||||
}
|
||||
if (validateEmails(forms)) {
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.Placeholders];
|
||||
|
||||
let pdfUrl = props.item?.URL || props.item?.SignedUrl;
|
||||
const prefillExist = Placeholders?.some(
|
||||
(data) => data.Role === "prefill"
|
||||
);
|
||||
if (prefillExist) {
|
||||
const updatedPrefills = updateWidgetValues(
|
||||
prefillWidgets,
|
||||
Placeholders
|
||||
);
|
||||
const prefillDetails = updatedPrefills?.find(
|
||||
(data) => data.Role === "prefill"
|
||||
);
|
||||
const pdfSignedUrl = await getSignedUrl(
|
||||
pdfUrl,
|
||||
"", //docId
|
||||
props.item.objectId, // templateId
|
||||
);
|
||||
const pdfArrayBuffer = await loadPdfOnce(pdfSignedUrl);
|
||||
if (pdfArrayBuffer === "Error") {
|
||||
const error = t("something-went-wrong-mssg");
|
||||
alert(error);
|
||||
dispatch(setBulkLoader(false));
|
||||
return;
|
||||
}
|
||||
pdfUrl = await handleEmbedPrefillToDoc(
|
||||
prefillDetails,
|
||||
1, // scale
|
||||
pdfArrayBuffer,
|
||||
[], // prefillImg,
|
||||
props.item?.ExtUserPtr?.UserId?.objectId // userId
|
||||
);
|
||||
|
||||
if (pdfUrl?.error) {
|
||||
const error = pdfUrl?.error?.includes("not compatible")
|
||||
? t("pdf-uncompatible", { appName: appName })
|
||||
: pdfUrl?.error;
|
||||
alert(error);
|
||||
dispatch(setBulkLoader(false));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
let error = "";
|
||||
// Loop through each form
|
||||
for (const form of forms) {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: normalizeKey(field.email),
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
const widgetValues = form?.fields?.flatMap((f) => f.widgets);
|
||||
const updateWidgetPlaceholder = updateWidgetValues(
|
||||
widgetValues,
|
||||
updatedPlaceholders
|
||||
);
|
||||
let signedUrl = pdfUrl;
|
||||
const placeholders = removeWidgetValues(
|
||||
widgetValues,
|
||||
updateWidgetPlaceholder
|
||||
);
|
||||
if (!signedUrl) {
|
||||
error = t("something-went-wrong-mssg");
|
||||
dispatch(setBulkLoader(false));
|
||||
break;
|
||||
}
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
URL: signedUrl,
|
||||
Placeholders: placeholders,
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
URL: signedUrl,
|
||||
Placeholders: placeholders,
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
alert(error);
|
||||
} else {
|
||||
await batchQuery(Documents);
|
||||
}
|
||||
} else {
|
||||
dispatch(setBulkLoader(false));
|
||||
}
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const functionsUrl = `${localStorage.getItem("baseUrl")}functions/batchdocuments`;
|
||||
const headers = {
|
||||
...tokenHeader,
|
||||
};
|
||||
const params = { Documents: JSON.stringify(Documents) };
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
if (res.data && res.data.result) {
|
||||
setStep((s) => Math.min(2, s + 1));
|
||||
setStepKey("response");
|
||||
setMessage({
|
||||
status: "success",
|
||||
message: "Documents sent successfully."
|
||||
});
|
||||
// props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
const message =
|
||||
err?.response?.data?.error || err?.message || "something went wrong.";
|
||||
console.error("Error sending documents:", message);
|
||||
setMessage({ status: "failed", message });
|
||||
// props.handleClose("error", 0, message);
|
||||
} finally {
|
||||
dispatch(setBulkLoader(false));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isLoader) {
|
||||
return (
|
||||
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
isVacantRoles ? (
|
||||
<>
|
||||
<div className="border-t mt-3" />
|
||||
{/* Wizard header */}
|
||||
<div className="px-6 py-3">
|
||||
<WizardHeader
|
||||
steps={stepsList}
|
||||
step={step}
|
||||
// onStepClick={(i) => i <= step && setStep(i)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{stepKey === "prefill" && (
|
||||
<PrefillWidgets
|
||||
prefills={prefillWidgets}
|
||||
setPrefills={setPrefillWidgets}
|
||||
onNext={() => {
|
||||
setStep((s) => Math.min(2, s + 1));
|
||||
setStepKey("recipients");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{stepKey === "recipients" && (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-auto">
|
||||
{
|
||||
forms?.length > 0 && (
|
||||
<div className="mx-4">
|
||||
<Table
|
||||
headers={headers}
|
||||
rowData={forms}
|
||||
handleInputChange={handleInputChange}
|
||||
handleWidgetDetails={handleWidgetDetails}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap pb-3 pt-2 px-3 gap-3 justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-accent w-[150px] focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>
|
||||
<span>{t("send")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
{stepKey === "response" && (
|
||||
<ResponseTab
|
||||
prefillCount={prefillWidgets?.length}
|
||||
documentCount={forms?.length}
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-1")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-2")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-3")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,62 @@
|
||||
import RenderWidgets from "./RenderWidgets";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PrefillWidgets = ({ prefills = [], setPrefills, onNext }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleWidgetDetails = (value, widgetIndex) => {
|
||||
setPrefills((prev) => {
|
||||
const widgets = [...(prev ?? [])];
|
||||
const w = widgets[widgetIndex];
|
||||
if (!w) return prev;
|
||||
widgets[widgetIndex] = {
|
||||
...w,
|
||||
options: { ...w.options, response: value },
|
||||
response: value
|
||||
};
|
||||
return widgets;
|
||||
});
|
||||
};
|
||||
|
||||
const handleNext = (e) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
onNext?.();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{prefills?.length > 0 && (
|
||||
<form
|
||||
onSubmit={handleNext}
|
||||
className="m-3 md:m-6 text-base-content flex flex-col relative"
|
||||
>
|
||||
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
|
||||
{prefills.map((widget, index) => (
|
||||
<RenderWidgets
|
||||
key={widget.key}
|
||||
showLabel
|
||||
widget={widget}
|
||||
handleWidgetDetails={(value) =>
|
||||
handleWidgetDetails(value, index)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap mt-3 gap-3 justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-primary w-[150px] focus:outline-none"
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrefillWidgets;
|
||||
@@ -0,0 +1,646 @@
|
||||
import { forwardRef, memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import moment from "moment";
|
||||
import {
|
||||
changeDateToMomentFormat,
|
||||
compressedFileSize,
|
||||
generateId,
|
||||
getMonth,
|
||||
getYear,
|
||||
months,
|
||||
radioButtonWidget,
|
||||
textInputWidget,
|
||||
textWidget,
|
||||
years
|
||||
} from "../../../constant/Utils";
|
||||
import PenColorComponent from "../../pdf/tab/PenColorComponent";
|
||||
import { getDatePickerDate, toHtmlPattern } from "../../../utils";
|
||||
import CellsInput from "../../shared/fields/CellsInput";
|
||||
import { emailRegex } from "../../../constant/const";
|
||||
|
||||
const inputOpt = new Set(["text", "email", "number"]);
|
||||
const PREFILL_PREFIX = "prefill::";
|
||||
|
||||
const inputTypes = {
|
||||
email: emailRegex,
|
||||
number: /^[0-9\s]*$/,
|
||||
ssn: /^(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}$/
|
||||
};
|
||||
const inputValidation = (pattern, type) => {
|
||||
if (pattern) return pattern;
|
||||
return inputTypes[type] || "";
|
||||
};
|
||||
const canavasTheme = `opensigncss:bg-white opensigndark:bg-[#121212] opensigndark:border-[#f6f3f4]/20 opensigncss:border-gray-300 opensigndark:hover:border-white opensigncss:hover:border-black border-[1px]`;
|
||||
|
||||
const widgetLabelCss = (isRequired = false) => {
|
||||
return `${isRequired ? "after:content-['_*'] after:text-red-500" : ""} block text-xs font-semibold`;
|
||||
};
|
||||
|
||||
const DateWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const format = widget?.options?.validation?.format || "MM/dd/yyyy";
|
||||
|
||||
const PrefillDateInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<div
|
||||
style={{ fontFamily: "Arial, sans-serif" }}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full"
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
{value ? value : format}
|
||||
<i className="fa-light fa-calendar ml-[5px]"></i>
|
||||
</div>
|
||||
));
|
||||
PrefillDateInput.displayName = "PrefillDateInput";
|
||||
|
||||
const handleDate = (widget) => {
|
||||
// The getDatePickerDate function retrieves the date in the correct format supported by the DatePicker.
|
||||
try {
|
||||
return getDatePickerDate(widget?.response, format);
|
||||
} catch (err) {
|
||||
console.error("handleDate error ", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//function to set date with required date format onchange date
|
||||
const handleOnDateChange = (date) => {
|
||||
let updateDate = date;
|
||||
let newDate;
|
||||
const isSpecialDateFormat =
|
||||
format && ["dd-MM-yyyy", "dd.MM.yyyy", "dd/MM/yyyy"].includes(format);
|
||||
if (isSpecialDateFormat) {
|
||||
newDate = moment(updateDate).format(changeDateToMomentFormat(format));
|
||||
} else {
|
||||
//using moment package is used to change date as per the format provided in selectDate obj e.g. - MM/dd/yyyy -> 03/12/2024
|
||||
newDate = new Date(updateDate);
|
||||
newDate = moment(newDate.getTime()).format(
|
||||
changeDateToMomentFormat(format)
|
||||
);
|
||||
}
|
||||
onChange(newDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-max">
|
||||
<DatePicker
|
||||
portalId="datepicker-portal-root"
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
renderCustomHeader={({ date, changeYear, changeMonth }) => (
|
||||
<div className="flex justify-start ml-2">
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={months[getMonth(date)]}
|
||||
onChange={({ target: { value } }) =>
|
||||
changeMonth(months.indexOf(value))
|
||||
}
|
||||
>
|
||||
{months.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="bg-transparent outline-none"
|
||||
value={getYear(date)}
|
||||
onChange={({ target: { value } }) => changeYear(value)}
|
||||
>
|
||||
{years.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
wrapperClassName="w-full"
|
||||
closeOnScroll={true}
|
||||
selected={handleDate(widget)}
|
||||
onChange={(date) => handleOnDateChange(date, widget)}
|
||||
customInput={<PrefillDateInput />}
|
||||
dateFormat={widget?.options?.validation?.format || "MM/dd/yyyy"}
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const TextWidget = ({
|
||||
widget,
|
||||
isRequired,
|
||||
onChange,
|
||||
showLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const inputType = widget?.options?.validation?.type || "";
|
||||
const serverRegex = widget?.options?.validation?.pattern;
|
||||
const isPredfineType = inputType ? inputOpt?.has(inputType) : "";
|
||||
const regExpression = inputValidation(serverRegex, inputType);
|
||||
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
|
||||
const [value, setValue] = useState(() => {
|
||||
const response =
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
|
||||
return inputType === "number" && response ? Number(response) : response;
|
||||
});
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const text = e.target.value || "";
|
||||
setValue(text);
|
||||
onChange(text);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-max">
|
||||
<input
|
||||
type={isPredfineType ? inputType : "text"}
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
value={value}
|
||||
placeholder={
|
||||
widget?.options?.hint ||
|
||||
t("enter-value", { value: widget?.options?.name })
|
||||
}
|
||||
className={[
|
||||
"op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs",
|
||||
].join(" ")}
|
||||
onChange={(e) => handleInputChange(e)}
|
||||
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
|
||||
onInvalid={(e) => {
|
||||
const el = e.currentTarget;
|
||||
// ✅ Only override message if pattern exists AND the error is patternMismatch
|
||||
if (pattern && el.validity.patternMismatch) {
|
||||
el.setCustomValidity(t("validation-alert-1"));
|
||||
} else {
|
||||
el.setCustomValidity("");
|
||||
}
|
||||
}}
|
||||
onInput={(e) => {
|
||||
e.currentTarget.setCustomValidity("");
|
||||
}}
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const CheckboxWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedCheckbox, setSelectedCheckbox] = useState(
|
||||
() => widget?.options?.response || widget?.options?.defaultValue || []
|
||||
);
|
||||
const groupName = `checkbox-group-${widget.key}`;
|
||||
const noneSelected = (selectedCheckbox?.length ?? 0) === 0;
|
||||
|
||||
const handleCheckboxValue = (isChecked, ind) => {
|
||||
// ✅ clear hidden input error immediately
|
||||
const hidden = document.querySelector(`input[name="${groupName}"]`);
|
||||
hidden?.setCustomValidity("");
|
||||
|
||||
const prevArr = Array.isArray(selectedCheckbox) ? selectedCheckbox : [];
|
||||
|
||||
const updateResponse = isChecked
|
||||
? prevArr.includes(ind)
|
||||
? prevArr
|
||||
: [...prevArr, ind] // add once
|
||||
: prevArr.filter((v) => v !== ind); // remove
|
||||
|
||||
setSelectedCheckbox(updateResponse);
|
||||
onChange(updateResponse);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-y-1 min-w-max relative">
|
||||
{widget.options?.values?.map((value, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="select-none-cls flex items-center text-center gap-0.5"
|
||||
>
|
||||
<input
|
||||
id={`checkbox-${widget.key + ind}`}
|
||||
className="mt-[2px] op-checkbox op-checkbox-xs"
|
||||
type="checkbox"
|
||||
checked={selectedCheckbox.includes(ind)}
|
||||
onChange={(e) => handleCheckboxValue(e.target.checked, ind)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`checkbox-${widget.key + ind}`}
|
||||
className="text-xs mb-0 text-center ml-[3px] cursor-pointer"
|
||||
>
|
||||
{value}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{/* ✅ the validator */}
|
||||
{isRequired && (
|
||||
<input
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="absolute opacity-0 pointer-events-none w-0.5 h-0.5 left-[0.46rem] top-4"
|
||||
name={groupName}
|
||||
value={noneSelected ? "" : "selected"} // empty => invalid, non-empty => valid
|
||||
required
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("select-at-least-option"))
|
||||
}
|
||||
onChange={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const DropdownWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState(
|
||||
() =>
|
||||
widget?.options?.response?.trim() ||
|
||||
widget?.options?.defaultValue?.trim() ||
|
||||
""
|
||||
);
|
||||
|
||||
const handleDropdownChange = (value) => {
|
||||
const select = value?.trim();
|
||||
setSelected(select);
|
||||
onChange(select);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-max">
|
||||
<select
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-base-content w-full"
|
||||
id={`widget-${widget?.options?.name}-${widget?.key}`}
|
||||
value={selected}
|
||||
onChange={(e) => handleDropdownChange(e.target.value)}
|
||||
required={isRequired}
|
||||
>
|
||||
{/* Default/Title option */}
|
||||
<option value="" disabled hidden>
|
||||
{t("choose-one")}
|
||||
</option>
|
||||
{widget?.options?.values?.map((option, ind) => (
|
||||
<option key={ind} value={option?.trim()}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const RadioButtonWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const [selected, setSelected] = useState(
|
||||
() =>
|
||||
widget?.options?.response?.trim() ||
|
||||
widget?.options?.defaultValue?.trim() ||
|
||||
""
|
||||
);
|
||||
const id = generateId(4);
|
||||
|
||||
const handleRadioChange = (value) => {
|
||||
const select = value?.trim();
|
||||
setSelected(select);
|
||||
onChange(select);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-y-1 min-w-max">
|
||||
{widget.options?.values?.map((data, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="select-none-cls flex items-center text-center gap-0.5"
|
||||
>
|
||||
<input
|
||||
id={`radio-${widget.key + ind}`}
|
||||
className="mt-[2px] op-radio op-radio-xs"
|
||||
name={`radio-group-${widget.key}-${id}`} // ✅ same name => browser treats all radios as ONE group
|
||||
type="radio"
|
||||
required={isRequired && ind === 0} // ✅ set required ONCE (on first radio) to make the whole group mandatory
|
||||
checked={selected === data?.trim()}
|
||||
onChange={() => handleRadioChange(data)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`radio-${widget.key + ind}`}
|
||||
className="text-xs mb-0 ml-[2px] cursor-pointer"
|
||||
>
|
||||
{data}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ImageWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const imageRef = useRef(null);
|
||||
const [img, setImg] = useState({ src: "", imgType: "" });
|
||||
const [defaultImg, setDefaultImg] = useState({ src: "", imgType: "" });
|
||||
|
||||
useEffect(() => {
|
||||
if (widget?.response) {
|
||||
setDefaultImg({ src: widget?.response, imgType: "" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onImageChange = (e) => {
|
||||
if (e.target.files && e.target.files?.[0]) {
|
||||
const file = e.target.files?.[0];
|
||||
compressedFileSize(file, ({ src, imgType }) => {
|
||||
onChange(src);
|
||||
setImg({ src, imgType });
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleClearImage = () => {
|
||||
setDefaultImg({ src: "", imgType: "" });
|
||||
setImg({ src: "", imgType: "" });
|
||||
onChange("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col prefillCanvas">
|
||||
{defaultImg?.src || img?.src ? (
|
||||
<div
|
||||
className={`cursor-pointer rounded-box ${canavasTheme} flex flex-col w-full h-full justify-center items-center`}
|
||||
>
|
||||
<img
|
||||
alt={`image_${widget?.options?.name}`}
|
||||
src={img.src || defaultImg?.src}
|
||||
draggable="false"
|
||||
className="object-contain h-full w-full aspect-[5/2]"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`cursor-pointer rounded-box ${canavasTheme} overflow-hidden w-full h-full aspect-[5/2] flex flex-col justify-center items-center`}
|
||||
onClick={() => imageRef.current?.click()}
|
||||
>
|
||||
<i className="fa-light text-base-content fa-cloud-upload-alt text-[25px]"></i>
|
||||
<div className="text-[10px] text-base-content">{t("upload")}</div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => onImageChange?.(e, widget)}
|
||||
className="filetype w-[1px] h-[1px] opacity-0"
|
||||
accept="image/png,image/jpeg"
|
||||
ref={imageRef} // Assign ref dynamically
|
||||
required={isRequired}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(defaultImg?.src || img?.src) && (
|
||||
<span
|
||||
onClick={handleClearImage}
|
||||
className="flex justify-start text-blue-500 underline cursor-pointer ml-1"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const DrawWidget = ({ widget, isRequired, onChange, showLabel }) => {
|
||||
const { t } = useTranslation();
|
||||
const canvasRef = useRef(null);
|
||||
const sigRequiredRef = useRef(null);
|
||||
const [hasDraw, setHasDraw] = useState("");
|
||||
const [penColor, setPenColor] = useState("blue");
|
||||
const [image, setImage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (widget?.response) {
|
||||
setImage(widget?.response);
|
||||
setHasDraw("signed");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSignatureChange = () => {
|
||||
const draw = canvasRef.current?.isEmpty?.() ? "" : "signed";
|
||||
setHasDraw(draw);
|
||||
onChange(canvasRef.current.toDataURL());
|
||||
|
||||
// clear custom error once user signs
|
||||
sigRequiredRef.current?.setCustomValidity("");
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setImage("");
|
||||
if (canvasRef?.current) {
|
||||
canvasRef.current.clear();
|
||||
onChange("");
|
||||
setHasDraw("");
|
||||
// clear custom error once user signs
|
||||
sigRequiredRef.current?.setCustomValidity("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative w-fit">
|
||||
{image ? (
|
||||
<div className={`prefillCanvas ${canavasTheme} rounded-[10px]`}>
|
||||
<img
|
||||
alt={`draw_${widget?.options?.name}`}
|
||||
src={image}
|
||||
className="prefillCanvas object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SignatureCanvas
|
||||
ref={canvasRef}
|
||||
penColor={penColor}
|
||||
canvasProps={{
|
||||
className: `prefillCanvas ${canavasTheme} rounded-[10px]`
|
||||
}}
|
||||
onEnd={() => handleSignatureChange()}
|
||||
dotSize={1}
|
||||
/>
|
||||
)}
|
||||
{/* ✅ Native form validation hook */}
|
||||
<input
|
||||
ref={sigRequiredRef}
|
||||
type="text"
|
||||
value={hasDraw}
|
||||
required={isRequired}
|
||||
tabIndex={-1}
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("draw-required"))}
|
||||
onChange={() => {}}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between mt-[8px] w-[200px]">
|
||||
<PenColorComponent
|
||||
penColor={penColor}
|
||||
setPenColor={setPenColor}
|
||||
hideLabel
|
||||
penSize="sm"
|
||||
/>
|
||||
<span
|
||||
onClick={handleClear}
|
||||
className="flex justify-start text-blue-500 underline cursor-pointer"
|
||||
>
|
||||
{t("clear")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CellsWidget = ({
|
||||
widget,
|
||||
isRequired,
|
||||
onChange,
|
||||
showLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const count = widget?.options?.cellCount || 1;
|
||||
const [word, setWord] = useState("");
|
||||
const inputType = widget?.options?.validation?.type || "";
|
||||
const serverRegex = widget?.options?.validation?.pattern;
|
||||
const regExpression = inputValidation(serverRegex, inputType);
|
||||
const pattern = useMemo(() => toHtmlPattern(regExpression), [regExpression]);
|
||||
|
||||
useEffect(() => {
|
||||
const response =
|
||||
widget?.options?.response ?? widget?.options?.defaultValue ?? "";
|
||||
const isNumber = inputType === "number" && response;
|
||||
setWord(isNumber ? Number(response) : response);
|
||||
}, []);
|
||||
|
||||
const handleChange = (words) => {
|
||||
const value = words;
|
||||
setWord(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{showLabel && (
|
||||
<div className={`${widgetLabelCss(isRequired)} mb-[0.5rem]`}>
|
||||
{widget?.options?.name}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<CellsInput
|
||||
hint={
|
||||
widget?.options?.hint ||
|
||||
t("enter-value", { value: widget?.options?.name })
|
||||
}
|
||||
count={count}
|
||||
value={word}
|
||||
onChange={handleChange}
|
||||
only="all"
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
id={`widget-${widget?.options?.name}-${widget.key}`}
|
||||
value={word}
|
||||
required={isRequired}
|
||||
tabIndex={-1}
|
||||
onChange={() => {}}
|
||||
pattern={pattern || undefined} // if no pattern, browser won't do pattern validation
|
||||
onInvalid={(e) => {
|
||||
const el = e.currentTarget;
|
||||
// ✅ Only override message if pattern exists AND the error is patternMismatch
|
||||
if (pattern && el.validity.patternMismatch) {
|
||||
el.setCustomValidity(t("validation-alert-1"));
|
||||
} else {
|
||||
el.setCustomValidity("");
|
||||
}
|
||||
}}
|
||||
onInput={(e) => {
|
||||
e.currentTarget.setCustomValidity("");
|
||||
}}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 w-[1px] h-[1px] pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderWidgets = ({
|
||||
widget,
|
||||
handleWidgetDetails,
|
||||
showLabel = false,
|
||||
}) => {
|
||||
const label = widget?.label?.toLowerCase() ?? "";
|
||||
const isPrefill = label.includes(PREFILL_PREFIX);
|
||||
const commonProps = {
|
||||
widget,
|
||||
isRequired: widget.options?.status === "required" && isPrefill,
|
||||
showLabel: showLabel,
|
||||
onChange: handleWidgetDetails,
|
||||
};
|
||||
|
||||
switch (widget.type) {
|
||||
case "date":
|
||||
return <DateWidget {...commonProps} />;
|
||||
case textWidget:
|
||||
case textInputWidget:
|
||||
case "name":
|
||||
case "email":
|
||||
case "company":
|
||||
case "job title":
|
||||
case "number":
|
||||
return <TextWidget {...commonProps} />;
|
||||
case "checkbox":
|
||||
return <CheckboxWidget {...commonProps} />;
|
||||
case radioButtonWidget:
|
||||
return <RadioButtonWidget {...commonProps} />;
|
||||
case "dropdown":
|
||||
return <DropdownWidget {...commonProps} />;
|
||||
case "image":
|
||||
case "stamp":
|
||||
return <ImageWidget {...commonProps} />;
|
||||
case "initials":
|
||||
case "draw":
|
||||
return <DrawWidget {...commonProps} />;
|
||||
case "cells":
|
||||
return <CellsWidget {...commonProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(RenderWidgets);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function ResponseTab({
|
||||
prefillCount = 0,
|
||||
documentCount = 0,
|
||||
message = { status: "", message: "" }
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { status, message: msg } = message;
|
||||
return (
|
||||
<div className="rounded-box border p-4 m-3 md:m-6 ">
|
||||
<div className="font-medium">{t("summary")}</div>
|
||||
<ul className="mt-2 list-disc pl-5 text-base-content/80">
|
||||
<li>
|
||||
{t("prefill-fields")}: {prefillCount}
|
||||
</li>
|
||||
<li>
|
||||
{t("documents")}: {documentCount}
|
||||
</li>
|
||||
<li>
|
||||
{t("status")}: {status}
|
||||
</li>
|
||||
{msg && (
|
||||
<li>
|
||||
{t("message")}: {msg}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResponseTab;
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
useState
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SuggestionInput from "../../shared/fields/SuggestionInput";
|
||||
import RenderWidgets from "./RenderWidgets";
|
||||
|
||||
const requiredAsteriskCls = (isRequired = false) => {
|
||||
return isRequired ? "after:content-['_*'] after:text-red-500" : "";
|
||||
};
|
||||
const Table = ({
|
||||
headers = [],
|
||||
rowData = [],
|
||||
handleInputChange,
|
||||
handleWidgetDetails,
|
||||
initialPerPage = 25,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(initialPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentData = rowData.slice(startIndex, endIndex);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
||||
{/* Table */}
|
||||
<div
|
||||
className="overflow-x-auto border border-gray-300 rounded-lg"
|
||||
>
|
||||
<table className="op-table border-collapse w-full">
|
||||
<thead className="text-[13px] text-center">
|
||||
<tr className="border-y-[1px]">
|
||||
{headers?.map((header) => (
|
||||
<th
|
||||
key={header.label}
|
||||
className={`${requiredAsteriskCls(header?.isRequired)} p-2`}
|
||||
>
|
||||
{header.label}
|
||||
</th>
|
||||
))}
|
||||
{rowData.length > 1 && <th className="p-2">{t("action")}</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentData.length > 0 &&
|
||||
currentData.map((form, formIndex) => {
|
||||
const globalIndex = startIndex + formIndex;
|
||||
const fields = form?.fields ?? [];
|
||||
|
||||
const emailFields = fields.filter((f) => f.label !== "prefill");
|
||||
|
||||
const widgets = fields.flatMap((f, fieldIndex) =>
|
||||
(f.widgets ?? []).map((widget, widgetIndex) => ({
|
||||
widget,
|
||||
fieldIndex, // keep the fieldIndex that produced this widget
|
||||
widgetIndex
|
||||
}))
|
||||
);
|
||||
return (
|
||||
<tr key={form.Id} id={`table-row-${formIndex}`}>
|
||||
{emailFields.map((field, fieldIndex) => (
|
||||
<td key={field.fieldId} className="p-2">
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.email ?? ""}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(globalIndex, signer, field.label)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
||||
{widgets.map(({ widget, fieldIndex, widgetIndex }) => (
|
||||
<td key={`${form.Id}-${widget.key}`} className="p-2">
|
||||
<RenderWidgets
|
||||
widget={widget}
|
||||
handleWidgetDetails={(value) =>
|
||||
handleWidgetDetails(
|
||||
value,
|
||||
globalIndex, // formIndex,
|
||||
fieldIndex,
|
||||
widgetIndex
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -0,0 +1,74 @@
|
||||
/* ---------------- Wizard Header ---------------- */
|
||||
|
||||
import React from "react";
|
||||
|
||||
function WizardHeader({ steps, step, onStepClick }) {
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row flex-wrap items-center justify-center gap-0 md:gap-2">
|
||||
{steps.map((s, i) => {
|
||||
const isActive = i === step;
|
||||
const isDone = i < step;
|
||||
|
||||
return (
|
||||
<React.Fragment key={s.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStepClick?.(i)}
|
||||
className={[
|
||||
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-xs lg:text-sm font-medium border focus:outline-none",
|
||||
isActive
|
||||
? "op-bg-primary text-primary-content border-primary"
|
||||
: isDone
|
||||
? "bg-base-100 text-base-content border-base-300 hover:bg-base-200"
|
||||
: "bg-base-100 text-base-content/70 border-base-300"
|
||||
].join(" ")}
|
||||
title={s.label}
|
||||
>
|
||||
<span
|
||||
className={[
|
||||
"inline-flex h-5 w-5 items-center justify-center rounded-full text-xs",
|
||||
isActive
|
||||
? "op-bg-primary-content/15 text-primary-content"
|
||||
: isDone
|
||||
? "bg-base-200 text-base-content"
|
||||
: "bg-base-200 text-base-content/70"
|
||||
].join(" ")}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
|
||||
<span className="whitespace-nowrap">{s.label}</span>
|
||||
|
||||
{/* info icon like screenshot */}
|
||||
<a data-tooltip-id={`${s.key}-tooltip`} className="ml-1">
|
||||
<span
|
||||
className={[
|
||||
"inline-flex h-[1.10rem] w-[1.10rem] items-center justify-center border-[1px] rounded-full",
|
||||
!isActive
|
||||
? "border-[#33bbff] text-[#33bbff]"
|
||||
: "border border-base-300 text-base-300"
|
||||
].join(" ")}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<i className="fa-light fa-question rounded-full text-[13px]"></i>
|
||||
</span>
|
||||
</a>
|
||||
<span className="text-left">{s.help}</span>
|
||||
</button>
|
||||
|
||||
{i < steps.length - 1 && (
|
||||
<span
|
||||
className="mx-1 select-none text-base-content/40 text-center text-2xl md:mb-2 rotate-90 md:rotate-0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
›
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WizardHeader;
|
||||
@@ -29,7 +29,7 @@ const DashboardButton = (props) => {
|
||||
: "cursor-default"
|
||||
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center text-base-content">
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
|
||||
<i
|
||||
@@ -39,7 +39,7 @@ const DashboardButton = (props) => {
|
||||
></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-lg ml-3 text-base-content">
|
||||
<div className="text-lg ml-3">
|
||||
{t(`sidebar.${props.Label}`)}
|
||||
{props.Label === "Sign yourself" && (
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Parse from "parse";
|
||||
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
||||
import { useNavigate } from "react-router";
|
||||
import Tooltip from "../../primitives/Tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
|
||||
const DashboardCard = (props) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -14,7 +15,7 @@ const DashboardCard = (props) => {
|
||||
const [response, setresponse] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const renderData = async () => {
|
||||
const renderData = withSessionValidation(async () => {
|
||||
if (props.Data.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -148,7 +149,7 @@ const DashboardCard = (props) => {
|
||||
);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers.some(
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser.id
|
||||
);
|
||||
if (isSigner) {
|
||||
@@ -168,31 +169,27 @@ const DashboardCard = (props) => {
|
||||
}
|
||||
}
|
||||
setresponse(arr.length);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
await axios.get(url, { headers: headers }).then((res) => {
|
||||
if (res.data.results.length > 0) {
|
||||
setLoading(false);
|
||||
if (props.Data.key !== "count") {
|
||||
setresponse(res.data.results[0][props.Data.key]);
|
||||
} else {
|
||||
setresponse(res.data[props.Data.key]);
|
||||
}
|
||||
if (res?.data?.[props.Data.key]) {
|
||||
setresponse(parseInt(res.data[props.Data.key]));
|
||||
} else if (res?.data?.results?.length > 0) {
|
||||
setresponse(res.data.results.length);
|
||||
} else {
|
||||
setresponse(0);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Problem", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const filterRender = async () => {
|
||||
const filterRender = withSessionValidation(async () => {
|
||||
if (props.FilterData && props.FilterData.queryType === "function") {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -270,7 +267,7 @@ const DashboardCard = (props) => {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const setFormat = (val) => {
|
||||
switch (props.Format) {
|
||||
@@ -1,158 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Parse from "parse";
|
||||
import ReportTable from "../../primitives/GetReportDisplay";
|
||||
import reportJson from "../../json/ReportJson";
|
||||
import axios from "axios";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
function DashboardReport(props) {
|
||||
const { t } = useTranslation();
|
||||
// console.log("props ", props)
|
||||
const [List, setList] = useState([]);
|
||||
const [isLoader, setIsLoader] = useState(true);
|
||||
const [reportName, setReportName] = useState("");
|
||||
const [actions, setActions] = useState([]);
|
||||
const [heading, setHeading] = useState([]);
|
||||
const [isNextRecord, setIsNextRecord] = useState(false);
|
||||
const [isMoreDocs, setIsMoreDocs] = useState(true);
|
||||
const abortController = new AbortController();
|
||||
const docPerPage = 5;
|
||||
|
||||
useEffect(() => {
|
||||
setReportName("");
|
||||
getReportData(props.Record.reportId);
|
||||
|
||||
// Function returned from useEffect is called on unmount
|
||||
return () => {
|
||||
setIsLoader(true);
|
||||
setList([]);
|
||||
setIsNextRecord(false);
|
||||
// Here it'll abort the fetch
|
||||
abortController.abort();
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [props.Record.reportId]);
|
||||
|
||||
// below useEffect call when isNextRecord state is true and fetch next record
|
||||
useEffect(() => {
|
||||
if (isNextRecord) {
|
||||
getReportData(props.Record.reportId, List.length, 20);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
const getReportData = async (id, skipUserRecord = 0, limit = 20) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
setReportName(json.reportName);
|
||||
setHeading(json.heading);
|
||||
const currentUser = Parse.User.current().id;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
if (id === "5Go51Q7T8r") {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setIsNextRecord(false);
|
||||
if (!res.data.result.error) {
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
setIsLoader(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
<div className="h-[250px] flex justify-center items-center">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{reportName ? (
|
||||
<ReportTable
|
||||
ReportName={reportName}
|
||||
List={List}
|
||||
setList={setList}
|
||||
actions={actions}
|
||||
heading={heading}
|
||||
setIsNextRecord={setIsNextRecord}
|
||||
isMoreDocs={isMoreDocs}
|
||||
docPerPage={docPerPage}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
|
||||
<div className="text-center">
|
||||
<p className="text-xl text-black">{t("report-not-found")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardReport;
|
||||
@@ -0,0 +1,229 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import Parse from "parse";
|
||||
import DocumentsReport from "../../reports/document/DocumentsReport";
|
||||
import reportJson from "../../json/ReportJson";
|
||||
import axios from "axios";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withSessionValidation } from "../../utils";
|
||||
function DashboardReport(props) {
|
||||
const { t } = useTranslation();
|
||||
const [List, setList] = useState([]);
|
||||
const [isLoader, setIsLoader] = useState(true);
|
||||
const [reportName, setReportName] = useState("");
|
||||
const [actions, setActions] = useState([]);
|
||||
const [heading, setHeading] = useState([]);
|
||||
const [isNextRecord, setIsNextRecord] = useState(false);
|
||||
const [isMoreDocs, setIsMoreDocs] = useState(true);
|
||||
const abortController = new AbortController();
|
||||
const docPerPage = 5;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
||||
const [isSearchResult, setIsSearchResult] = useState(false);
|
||||
const debounceTimer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setReportName("");
|
||||
setSearchTerm("");
|
||||
setMobileSearchOpen(false);
|
||||
getReportData(props.Record.reportId, 0, 20, "");
|
||||
|
||||
// Function returned from useEffect is called on unmount
|
||||
return () => {
|
||||
setIsLoader(true);
|
||||
setList([]);
|
||||
setIsNextRecord(false);
|
||||
// Here it'll abort the fetch
|
||||
abortController.abort();
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [props.Record.reportId]);
|
||||
|
||||
// below useEffect call when isNextRecord state is true and fetch next record
|
||||
useEffect(() => {
|
||||
if (isNextRecord) {
|
||||
getReportData(props.Record.reportId, List.length, 20, searchTerm);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
const handleSearchChange = withSessionValidation(async (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
setSearchTerm(term);
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
debounceTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(
|
||||
url,
|
||||
{
|
||||
reportId: props.Record.reportId,
|
||||
searchTerm: term,
|
||||
skip: 0,
|
||||
limit: docPerPage
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
const data = res.data?.result || [];
|
||||
if (!data.error) {
|
||||
setList(data);
|
||||
setIsMoreDocs(data.length >= docPerPage);
|
||||
setIsNextRecord(false);
|
||||
setIsSearchResult(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Search error:", err);
|
||||
}
|
||||
}, 300);
|
||||
setIsSearchResult(false);
|
||||
});
|
||||
|
||||
const handleSearchPaste = (e) => {
|
||||
setTimeout(() => {
|
||||
handleSearchChange({ target: { value: e.target.value } });
|
||||
}, 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getReportData = withSessionValidation(
|
||||
async (id, skipUserRecord = 0, limit = 20, term = searchTerm) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
setActions(json.actions);
|
||||
setReportName(json.reportName);
|
||||
setHeading(json.heading);
|
||||
const currentUser = Parse.User.current().id;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
});
|
||||
if (id === "5Go51Q7T8r") {
|
||||
const listData = res.data?.result.filter(
|
||||
(x) => x.Signers.length > 0
|
||||
);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
let isRecord;
|
||||
if (obj?.AuditTrail && obj?.AuditTrail.length > 0) {
|
||||
isRecord = obj?.AuditTrail.some(
|
||||
(item) =>
|
||||
item?.UserPtr?.UserId?.objectId === currentUser &&
|
||||
item.Activity === "Signed"
|
||||
);
|
||||
} else {
|
||||
isRecord = false;
|
||||
}
|
||||
if (isRecord === false) {
|
||||
arr.push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arr.length === docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
}
|
||||
setIsNextRecord(false);
|
||||
if (!res.data.result.error) {
|
||||
setList((prevRecord) =>
|
||||
prevRecord.length > 0
|
||||
? [...prevRecord, ...res.data.result]
|
||||
: res.data.result
|
||||
);
|
||||
}
|
||||
}
|
||||
setIsLoader(false);
|
||||
} catch (err) {
|
||||
const isCancel = axios.isCancel(err);
|
||||
if (!isCancel) {
|
||||
console.log("err ", err);
|
||||
setIsLoader(false);
|
||||
}
|
||||
setIsLoader(false);
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
<div className="h-[250px] flex justify-center items-center">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{reportName ? (
|
||||
<DocumentsReport
|
||||
ReportName={reportName}
|
||||
List={List}
|
||||
setList={setList}
|
||||
actions={actions}
|
||||
heading={heading}
|
||||
setIsNextRecord={setIsNextRecord}
|
||||
isMoreDocs={isMoreDocs}
|
||||
docPerPage={docPerPage}
|
||||
mobileSearchOpen={mobileSearchOpen}
|
||||
setMobileSearchOpen={setMobileSearchOpen}
|
||||
searchTerm={searchTerm}
|
||||
handleSearchChange={handleSearchChange}
|
||||
handleSearchPaste={handleSearchPaste}
|
||||
isSearchResult={isSearchResult}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded-box">
|
||||
<div className="text-center text-xl text-base-content">
|
||||
{t("report-not-found")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardReport;
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { Suspense, lazy } from "react";
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import { lazyWithRetry } from "../../utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const DashboardButton = lazy(() => import("./DashboardButton"));
|
||||
const DashboardCard = lazy(() => import("./DashboardCard"));
|
||||
const DashboardReport = lazy(() => import("./DashboardReport"));
|
||||
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
|
||||
const DashboardCard = lazyWithRetry(() => import("./DashboardCard"));
|
||||
const DashboardReport = lazyWithRetry(() => import("./DashboardReport"));
|
||||
const buttonList = [
|
||||
{
|
||||
label: "Sign yourself",
|
||||
@@ -19,6 +20,7 @@ const buttonList = [
|
||||
];
|
||||
const GetDashboard = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const Button = ({ label, redirectId, redirectType, icon }) => (
|
||||
<DashboardButton
|
||||
Icon={icon}
|
||||
@@ -58,7 +60,9 @@ const GetDashboard = (props) => {
|
||||
<div data-tut={col.widget.data.tourSection}>
|
||||
<Suspense fallback={<div>please wait</div>}>
|
||||
<div className="mb-3 md:mb-0">
|
||||
<DashboardReport Record={col.widget} />
|
||||
<DashboardReport
|
||||
Record={col.widget}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -92,7 +96,9 @@ const GetDashboard = (props) => {
|
||||
return (
|
||||
<Suspense fallback={<div>please wait</div>}>
|
||||
<div className="mb-3 md:mb-0">
|
||||
<DashboardReport Record={col.widget} />
|
||||
<DashboardReport
|
||||
Record={col.widget}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
import { TEditorBlock } from '../../../documents/editor/core';
|
||||
import { setDocument, useDocument, useSelectedBlockId } from '../../../documents/editor/EditorContext';
|
||||
|
||||
import AvatarSidebarPanel from './input-panels/AvatarSidebarPanel';
|
||||
import ButtonSidebarPanel from './input-panels/ButtonSidebarPanel';
|
||||
import ColumnsContainerSidebarPanel from './input-panels/ColumnsContainerSidebarPanel';
|
||||
import ContainerSidebarPanel from './input-panels/ContainerSidebarPanel';
|
||||
import DividerSidebarPanel from './input-panels/DividerSidebarPanel';
|
||||
import EmailLayoutSidebarPanel from './input-panels/EmailLayoutSidebarPanel';
|
||||
import HeadingSidebarPanel from './input-panels/HeadingSidebarPanel';
|
||||
import HtmlSidebarPanel from './input-panels/HtmlSidebarPanel';
|
||||
import ImageSidebarPanel from './input-panels/ImageSidebarPanel';
|
||||
import SpacerSidebarPanel from './input-panels/SpacerSidebarPanel';
|
||||
import TextSidebarPanel from './input-panels/TextSidebarPanel';
|
||||
|
||||
function renderMessage(val: string) {
|
||||
return (
|
||||
<Box sx={{ m: 3, p: 1, border: '1px dashed', borderColor: 'divider' }}>
|
||||
<Typography color="text.secondary">{val}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ConfigurationPanel() {
|
||||
const document = useDocument();
|
||||
const selectedBlockId = useSelectedBlockId();
|
||||
|
||||
if (!selectedBlockId) {
|
||||
return renderMessage('Click on a block to inspect.');
|
||||
}
|
||||
const block = document[selectedBlockId];
|
||||
if (!block) {
|
||||
return renderMessage(`Block with id ${selectedBlockId} was not found. Click on a block to reset.`);
|
||||
}
|
||||
|
||||
const setBlock = (conf: TEditorBlock) => setDocument({ [selectedBlockId]: conf });
|
||||
const { data, type } = block;
|
||||
switch (type) {
|
||||
case 'Avatar':
|
||||
return <AvatarSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Button':
|
||||
return <ButtonSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'ColumnsContainer':
|
||||
return (
|
||||
<ColumnsContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />
|
||||
);
|
||||
case 'Container':
|
||||
return <ContainerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Divider':
|
||||
return <DividerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Heading':
|
||||
return <HeadingSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Html':
|
||||
return <HtmlSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Image':
|
||||
return <ImageSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'EmailLayout':
|
||||
return <EmailLayoutSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Spacer':
|
||||
return <SpacerSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
case 'Text':
|
||||
return <TextSidebarPanel key={selectedBlockId} data={data} setData={(data) => setBlock({ type, data })} />;
|
||||
default:
|
||||
return <pre>{JSON.stringify(block, null, ' ')}</pre>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { AspectRatioOutlined } from "@mui/icons-material";
|
||||
import { ToggleButton } from "@mui/material";
|
||||
import {
|
||||
AvatarProps,
|
||||
AvatarPropsDefaults,
|
||||
AvatarPropsSchema
|
||||
} from "@usewaypoint/block-avatar";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type AvatarSidebarPanelProps = {
|
||||
data: AvatarProps;
|
||||
setData: (v: AvatarProps) => void;
|
||||
};
|
||||
export default function AvatarSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: AvatarSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = AvatarPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const size = data.props?.size ?? AvatarPropsDefaults.size;
|
||||
const imageUrl = data.props?.imageUrl ?? AvatarPropsDefaults.imageUrl;
|
||||
const alt = data.props?.alt ?? AvatarPropsDefaults.alt;
|
||||
const shape = data.props?.shape ?? AvatarPropsDefaults.shape;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Avatar block">
|
||||
<SliderInput
|
||||
label="Size"
|
||||
iconLabel={<AspectRatioOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={3}
|
||||
min={32}
|
||||
max={256}
|
||||
defaultValue={size}
|
||||
onChange={(size) => {
|
||||
updateData({ ...data, props: { ...data.props, size } });
|
||||
}}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Shape"
|
||||
defaultValue={shape}
|
||||
onChange={(shape) => {
|
||||
updateData({ ...data, props: { ...data.props, shape } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="circle">Circle</ToggleButton>
|
||||
<ToggleButton value="square">Square</ToggleButton>
|
||||
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<TextInput
|
||||
label="Image URL"
|
||||
defaultValue={imageUrl}
|
||||
onChange={(imageUrl) => {
|
||||
updateData({ ...data, props: { ...data.props, imageUrl } });
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Alt text"
|
||||
defaultValue={alt}
|
||||
onChange={(alt) => {
|
||||
updateData({ ...data, props: { ...data.props, alt } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={["textAlign", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { ToggleButton } from "@mui/material";
|
||||
import {
|
||||
ButtonProps,
|
||||
ButtonPropsDefaults,
|
||||
ButtonPropsSchema
|
||||
} from "@usewaypoint/block-button";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColorInput from "./helpers/inputs/ColorInput";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ButtonSidebarPanelProps = {
|
||||
data: ButtonProps;
|
||||
setData: (v: ButtonProps) => void;
|
||||
};
|
||||
export default function ButtonSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: ButtonSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ButtonPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const text = data.props?.text ?? ButtonPropsDefaults.text;
|
||||
const url = data.props?.url ?? ButtonPropsDefaults.url;
|
||||
const fullWidth = data.props?.fullWidth ?? ButtonPropsDefaults.fullWidth;
|
||||
const size = data.props?.size ?? ButtonPropsDefaults.size;
|
||||
const buttonStyle =
|
||||
data.props?.buttonStyle ?? ButtonPropsDefaults.buttonStyle;
|
||||
const buttonTextColor =
|
||||
data.props?.buttonTextColor ?? ButtonPropsDefaults.buttonTextColor;
|
||||
const buttonBackgroundColor =
|
||||
data.props?.buttonBackgroundColor ??
|
||||
ButtonPropsDefaults.buttonBackgroundColor;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Button block">
|
||||
<TextInput
|
||||
label="Text"
|
||||
defaultValue={text}
|
||||
onChange={(text) =>
|
||||
updateData({ ...data, props: { ...data.props, text } })
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Url"
|
||||
defaultValue={url}
|
||||
onChange={(url) =>
|
||||
updateData({ ...data, props: { ...data.props, url } })
|
||||
}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Width"
|
||||
defaultValue={fullWidth ? "FULL_WIDTH" : "AUTO"}
|
||||
onChange={(v) =>
|
||||
updateData({
|
||||
...data,
|
||||
props: { ...data.props, fullWidth: v === "FULL_WIDTH" }
|
||||
})
|
||||
}
|
||||
>
|
||||
<ToggleButton value="FULL_WIDTH">Full</ToggleButton>
|
||||
<ToggleButton value="AUTO">Auto</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<RadioGroupInput
|
||||
label="Size"
|
||||
defaultValue={size}
|
||||
onChange={(size) =>
|
||||
updateData({ ...data, props: { ...data.props, size } })
|
||||
}
|
||||
>
|
||||
<ToggleButton value="x-small">Xs</ToggleButton>
|
||||
<ToggleButton value="small">Sm</ToggleButton>
|
||||
<ToggleButton value="medium">Md</ToggleButton>
|
||||
<ToggleButton value="large">Lg</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<RadioGroupInput
|
||||
label="Style"
|
||||
defaultValue={buttonStyle}
|
||||
onChange={(buttonStyle) =>
|
||||
updateData({ ...data, props: { ...data.props, buttonStyle } })
|
||||
}
|
||||
>
|
||||
<ToggleButton value="rectangle">Rectangle</ToggleButton>
|
||||
<ToggleButton value="rounded">Rounded</ToggleButton>
|
||||
<ToggleButton value="pill">Pill</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<ColorInput
|
||||
label="Text color"
|
||||
defaultValue={buttonTextColor}
|
||||
onChange={(buttonTextColor) =>
|
||||
updateData({ ...data, props: { ...data.props, buttonTextColor } })
|
||||
}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Button color"
|
||||
defaultValue={buttonBackgroundColor}
|
||||
onChange={(buttonBackgroundColor) =>
|
||||
updateData({
|
||||
...data,
|
||||
props: { ...data.props, buttonBackgroundColor }
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontWeight",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import {
|
||||
SpaceBarOutlined,
|
||||
VerticalAlignBottomOutlined,
|
||||
VerticalAlignCenterOutlined,
|
||||
VerticalAlignTopOutlined
|
||||
} from "@mui/icons-material";
|
||||
import { ToggleButton } from "@mui/material";
|
||||
|
||||
import ColumnsContainerPropsSchema, {
|
||||
ColumnsContainerProps
|
||||
} from "../../../../documents/blocks/ColumnsContainer/ColumnsContainerPropsSchema";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColumnWidthsInput from "./helpers/inputs/ColumnWidthsInput";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ColumnsContainerPanelProps = {
|
||||
data: ColumnsContainerProps;
|
||||
setData: (v: ColumnsContainerProps) => void;
|
||||
};
|
||||
export default function ColumnsContainerPanel({
|
||||
data,
|
||||
setData
|
||||
}: ColumnsContainerPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ColumnsContainerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Columns block">
|
||||
<RadioGroupInput
|
||||
label="Number of columns"
|
||||
defaultValue={data.props?.columnsCount === 2 ? "2" : "3"}
|
||||
onChange={(v) => {
|
||||
updateData({
|
||||
...data,
|
||||
props: { ...data.props, columnsCount: v === "2" ? 2 : 3 }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="2">2</ToggleButton>
|
||||
<ToggleButton value="3">3</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<ColumnWidthsInput
|
||||
defaultValue={data.props?.fixedWidths}
|
||||
onChange={(fixedWidths) => {
|
||||
updateData({ ...data, props: { ...data.props, fixedWidths } });
|
||||
}}
|
||||
/>
|
||||
<SliderInput
|
||||
label="Columns gap"
|
||||
iconLabel={<SpaceBarOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={80}
|
||||
defaultValue={data.props?.columnsGap ?? 0}
|
||||
onChange={(columnsGap) =>
|
||||
updateData({ ...data, props: { ...data.props, columnsGap } })
|
||||
}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Alignment"
|
||||
defaultValue={data.props?.contentAlignment ?? "middle"}
|
||||
onChange={(contentAlignment) => {
|
||||
updateData({ ...data, props: { ...data.props, contentAlignment } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="top">
|
||||
<VerticalAlignTopOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="middle">
|
||||
<VerticalAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="bottom">
|
||||
<VerticalAlignBottomOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from "react";
|
||||
import type { ZodError } from "zod";
|
||||
|
||||
import ContainerPropsSchema, {
|
||||
ContainerProps
|
||||
} from "../../../../documents/blocks/Container/ContainerPropsSchema";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ContainerSidebarPanelProps = {
|
||||
data: ContainerProps;
|
||||
setData: (v: ContainerProps) => void;
|
||||
};
|
||||
|
||||
export default function ContainerSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: ContainerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ContainerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<BaseSidebarPanel title="Container block">
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "borderColor", "borderRadius", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HeightOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
DividerProps,
|
||||
DividerPropsDefaults,
|
||||
DividerPropsSchema
|
||||
} from "@usewaypoint/block-divider";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColorInput from "./helpers/inputs/ColorInput";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type DividerSidebarPanelProps = {
|
||||
data: DividerProps;
|
||||
setData: (v: DividerProps) => void;
|
||||
};
|
||||
export default function DividerSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: DividerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
const updateData = (d: unknown) => {
|
||||
const res = DividerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
const lineColor = data.props?.lineColor ?? DividerPropsDefaults.lineColor;
|
||||
const lineHeight = data.props?.lineHeight ?? DividerPropsDefaults.lineHeight;
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Divider block">
|
||||
<ColorInput
|
||||
label="Color"
|
||||
defaultValue={lineColor}
|
||||
onChange={(lineColor) =>
|
||||
updateData({ ...data, props: { ...data.props, lineColor } })
|
||||
}
|
||||
/>
|
||||
<SliderInput
|
||||
label="Height"
|
||||
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={1}
|
||||
min={1}
|
||||
max={24}
|
||||
defaultValue={lineHeight}
|
||||
onChange={(lineHeight) =>
|
||||
updateData({ ...data, props: { ...data.props, lineHeight } })
|
||||
}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { WidthWide, RoundedCornerOutlined } from "@mui/icons-material";
|
||||
import EmailLayoutPropsSchema, {
|
||||
EmailLayoutProps
|
||||
} from "../../../../documents/blocks/EmailLayout/EmailLayoutPropsSchema";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import ColorInput, { NullableColorInput } from "./helpers/inputs/ColorInput";
|
||||
import { NullableFontFamily } from "./helpers/inputs/FontFamily";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
|
||||
type EmailLayoutSidebarFieldsProps = {
|
||||
data: EmailLayoutProps;
|
||||
setData: (v: EmailLayoutProps) => void;
|
||||
};
|
||||
export default function EmailLayoutSidebarFields({
|
||||
data,
|
||||
setData
|
||||
}: EmailLayoutSidebarFieldsProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = EmailLayoutPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Global">
|
||||
<ColorInput
|
||||
label="Backdrop color"
|
||||
defaultValue={data.backdropColor ?? "#F5F5F5"}
|
||||
onChange={(backdropColor) => updateData({ ...data, backdropColor })}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Canvas color"
|
||||
defaultValue={data.canvasColor ?? "#FFFFFF"}
|
||||
onChange={(canvasColor) => updateData({ ...data, canvasColor })}
|
||||
/>
|
||||
<NullableColorInput
|
||||
label="Canvas border color"
|
||||
defaultValue={data.borderColor ?? null}
|
||||
onChange={(borderColor) => updateData({ ...data, borderColor })}
|
||||
/>
|
||||
<SliderInput
|
||||
iconLabel={<WidthWide />}
|
||||
units="px"
|
||||
step={10}
|
||||
min={320}
|
||||
max={1200}
|
||||
label="Canvas width"
|
||||
defaultValue={data.canvasWidth ?? 600}
|
||||
onChange={(canvasWidth) => updateData({ ...data, canvasWidth })}
|
||||
/>
|
||||
<SliderInput
|
||||
iconLabel={<RoundedCornerOutlined />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={48}
|
||||
label="Canvas border radius"
|
||||
defaultValue={data.borderRadius ?? 0}
|
||||
onChange={(borderRadius) => updateData({ ...data, borderRadius })}
|
||||
/>
|
||||
<NullableFontFamily
|
||||
label="Font family"
|
||||
defaultValue="MODERN_SANS"
|
||||
onChange={(fontFamily) => updateData({ ...data, fontFamily })}
|
||||
/>
|
||||
<ColorInput
|
||||
label="Text color"
|
||||
defaultValue={data.textColor ?? "#262626"}
|
||||
onChange={(textColor) => updateData({ ...data, textColor })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { ToggleButton } from "@mui/material";
|
||||
import {
|
||||
HeadingProps,
|
||||
HeadingPropsDefaults,
|
||||
HeadingPropsSchema
|
||||
} from "@usewaypoint/block-heading";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type HeadingSidebarPanelProps = {
|
||||
data: HeadingProps;
|
||||
setData: (v: HeadingProps) => void;
|
||||
};
|
||||
export default function HeadingSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: HeadingSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = HeadingPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Heading block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={3}
|
||||
defaultValue={data.props?.text ?? HeadingPropsDefaults.text}
|
||||
onChange={(text) => {
|
||||
updateData({ ...data, props: { ...data.props, text } });
|
||||
}}
|
||||
/>
|
||||
<RadioGroupInput
|
||||
label="Level"
|
||||
defaultValue={data.props?.level ?? HeadingPropsDefaults.level}
|
||||
onChange={(level) => {
|
||||
updateData({ ...data, props: { ...data.props, level } });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="h1">H1</ToggleButton>
|
||||
<ToggleButton value="h2">H2</ToggleButton>
|
||||
<ToggleButton value="h3">H3</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontWeight",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HtmlProps, HtmlPropsSchema } from "@usewaypoint/block-html";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type HtmlSidebarPanelProps = {
|
||||
data: HtmlProps;
|
||||
setData: (v: HtmlProps) => void;
|
||||
};
|
||||
export default function HtmlSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: HtmlSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = HtmlPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Html block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={5}
|
||||
defaultValue={data.props?.contents ?? ""}
|
||||
onChange={(contents) =>
|
||||
updateData({ ...data, props: { ...data.props, contents } })
|
||||
}
|
||||
/>
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import {
|
||||
VerticalAlignBottomOutlined,
|
||||
VerticalAlignCenterOutlined,
|
||||
VerticalAlignTopOutlined
|
||||
} from "@mui/icons-material";
|
||||
import { Stack, ToggleButton } from "@mui/material";
|
||||
import { ImageProps, ImagePropsSchema } from "@usewaypoint/block-image";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import RadioGroupInput from "./helpers/inputs/RadioGroupInput";
|
||||
import TextDimensionInput from "./helpers/inputs/TextDimensionInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type ImageSidebarPanelProps = {
|
||||
data: ImageProps;
|
||||
setData: (v: ImageProps) => void;
|
||||
};
|
||||
export default function ImageSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: ImageSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = ImagePropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Image block">
|
||||
<TextInput
|
||||
label="Source URL"
|
||||
defaultValue={data.props?.url ?? ""}
|
||||
onChange={(v) => {
|
||||
const url = v.trim().length === 0 ? null : v.trim();
|
||||
updateData({ ...data, props: { ...data.props, url } });
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Alt text"
|
||||
defaultValue={data.props?.alt ?? ""}
|
||||
onChange={(alt) =>
|
||||
updateData({ ...data, props: { ...data.props, alt } })
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="Click through URL"
|
||||
defaultValue={data.props?.linkHref ?? ""}
|
||||
onChange={(v) => {
|
||||
const linkHref = v.trim().length === 0 ? null : v.trim();
|
||||
updateData({ ...data, props: { ...data.props, linkHref } });
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextDimensionInput
|
||||
label="Width"
|
||||
defaultValue={data.props?.width}
|
||||
onChange={(width) =>
|
||||
updateData({ ...data, props: { ...data.props, width } })
|
||||
}
|
||||
/>
|
||||
<TextDimensionInput
|
||||
label="Height"
|
||||
defaultValue={data.props?.height}
|
||||
onChange={(height) =>
|
||||
updateData({ ...data, props: { ...data.props, height } })
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<RadioGroupInput
|
||||
label="Alignment"
|
||||
defaultValue={data.props?.contentAlignment ?? "middle"}
|
||||
onChange={(contentAlignment) =>
|
||||
updateData({ ...data, props: { ...data.props, contentAlignment } })
|
||||
}
|
||||
>
|
||||
<ToggleButton value="top">
|
||||
<VerticalAlignTopOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="middle">
|
||||
<VerticalAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="bottom">
|
||||
<VerticalAlignBottomOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={["backgroundColor", "textAlign", "padding"]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { HeightOutlined } from "@mui/icons-material";
|
||||
import {
|
||||
SpacerProps,
|
||||
SpacerPropsDefaults,
|
||||
SpacerPropsSchema
|
||||
} from "@usewaypoint/block-spacer";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import SliderInput from "./helpers/inputs/SliderInput";
|
||||
|
||||
type SpacerSidebarPanelProps = {
|
||||
data: SpacerProps;
|
||||
setData: (v: SpacerProps) => void;
|
||||
};
|
||||
export default function SpacerSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: SpacerSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = SpacerPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Spacer block">
|
||||
<SliderInput
|
||||
label="Height"
|
||||
iconLabel={<HeightOutlined sx={{ color: "text.secondary" }} />}
|
||||
units="px"
|
||||
step={4}
|
||||
min={4}
|
||||
max={128}
|
||||
defaultValue={data.props?.height ?? SpacerPropsDefaults.height}
|
||||
onChange={(height) =>
|
||||
updateData({ ...data, props: { ...data.props, height } })
|
||||
}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { useState } from "react";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { TextProps, TextPropsSchema } from "@usewaypoint/block-text";
|
||||
|
||||
import BaseSidebarPanel from "./helpers/BaseSidebarPanel";
|
||||
import BooleanInput from "./helpers/inputs/BooleanInput";
|
||||
import TextInput from "./helpers/inputs/TextInput";
|
||||
import MultiStylePropertyPanel from "./helpers/style-inputs/MultiStylePropertyPanel";
|
||||
|
||||
type TextSidebarPanelProps = {
|
||||
data: TextProps;
|
||||
setData: (v: TextProps) => void;
|
||||
};
|
||||
export default function TextSidebarPanel({
|
||||
data,
|
||||
setData
|
||||
}: TextSidebarPanelProps) {
|
||||
const [, setErrors] = useState<ZodError | null>(null);
|
||||
|
||||
const updateData = (d: unknown) => {
|
||||
const res = TextPropsSchema.safeParse(d);
|
||||
if (res.success) {
|
||||
setData(res.data);
|
||||
setErrors(null);
|
||||
} else {
|
||||
setErrors(res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseSidebarPanel title="Text block">
|
||||
<TextInput
|
||||
label="Content"
|
||||
rows={5}
|
||||
defaultValue={data.props?.text ?? ""}
|
||||
onChange={(text) =>
|
||||
updateData({ ...data, props: { ...data.props, text } })
|
||||
}
|
||||
/>
|
||||
<BooleanInput
|
||||
label="Markdown (GitHub flavored)"
|
||||
defaultValue={data.props?.markdown ?? false}
|
||||
onChange={(markdown) =>
|
||||
updateData({ ...data, props: { ...data.props, markdown } })
|
||||
}
|
||||
/>
|
||||
|
||||
<MultiStylePropertyPanel
|
||||
names={[
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontWeight",
|
||||
"textAlign",
|
||||
"padding"
|
||||
]}
|
||||
value={data.style}
|
||||
onChange={(style) => updateData({ ...data, style })}
|
||||
/>
|
||||
</BaseSidebarPanel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
|
||||
type SidebarPanelProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
export default function BaseSidebarPanel({
|
||||
title,
|
||||
children
|
||||
}: SidebarPanelProps) {
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
color="text.secondary"
|
||||
sx={{ display: "block", mb: 2 }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack spacing={5} mb={3}>
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { FormControlLabel, Switch } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export default function BooleanInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<FormControlLabel
|
||||
label={label}
|
||||
control={
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={(_, checked: boolean) => {
|
||||
setValue(checked);
|
||||
onChange(checked);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { AddOutlined, CloseOutlined } from "@mui/icons-material";
|
||||
import { ButtonBase, InputLabel, Menu, Stack } from "@mui/material";
|
||||
|
||||
import Picker from "./Picker";
|
||||
|
||||
const BUTTON_SX = {
|
||||
border: "1px solid",
|
||||
borderColor: "cadet.400",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "4px",
|
||||
bgcolor: "#FFFFFF"
|
||||
};
|
||||
|
||||
type Props =
|
||||
| {
|
||||
nullable: true;
|
||||
label: string;
|
||||
onChange: (value: string | null) => void;
|
||||
defaultValue: string | null;
|
||||
}
|
||||
| {
|
||||
nullable: false;
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultValue: string;
|
||||
};
|
||||
export default function ColorInput({
|
||||
label,
|
||||
defaultValue,
|
||||
onChange,
|
||||
nullable
|
||||
}: Props) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const handleClickOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const renderResetButton = () => {
|
||||
if (!nullable) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setValue(null);
|
||||
onChange(null);
|
||||
}}
|
||||
>
|
||||
<CloseOutlined fontSize="small" sx={{ color: "grey.600" }} />
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOpenButton = () => {
|
||||
if (value) {
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={handleClickOpen}
|
||||
sx={{ ...BUTTON_SX, bgcolor: value }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ButtonBase onClick={handleClickOpen} sx={{ ...BUTTON_SX }}>
|
||||
<AddOutlined fontSize="small" />
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack alignItems="flex-start">
|
||||
<InputLabel sx={{ mb: 0.5 }}>{label}</InputLabel>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{renderOpenButton()}
|
||||
{renderResetButton()}
|
||||
</Stack>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
MenuListProps={{
|
||||
sx: { height: "auto", padding: 0 }
|
||||
}}
|
||||
>
|
||||
<Picker
|
||||
value={value || ""}
|
||||
onChange={(v) => {
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
|
||||
import { Box, Stack, SxProps } from "@mui/material";
|
||||
|
||||
import Swatch from "./Swatch";
|
||||
|
||||
const DEFAULT_PRESET_COLORS = [
|
||||
"#E11D48",
|
||||
"#DB2777",
|
||||
"#C026D3",
|
||||
"#9333EA",
|
||||
"#7C3AED",
|
||||
"#4F46E5",
|
||||
"#2563EB",
|
||||
"#0284C7",
|
||||
"#0891B2",
|
||||
"#0D9488",
|
||||
"#059669",
|
||||
"#16A34A",
|
||||
"#65A30D",
|
||||
"#CA8A04",
|
||||
"#D97706",
|
||||
"#EA580C",
|
||||
"#DC2626",
|
||||
"#FFFFFF",
|
||||
"#FAFAFA",
|
||||
"#F5F5F5",
|
||||
"#E5E5E5",
|
||||
"#D4D4D4",
|
||||
"#A3A3A3",
|
||||
"#737373",
|
||||
"#525252",
|
||||
"#404040",
|
||||
"#262626",
|
||||
"#171717",
|
||||
"#0A0A0A",
|
||||
"#000000"
|
||||
];
|
||||
|
||||
const SX: SxProps = {
|
||||
p: 1,
|
||||
".react-colorful__pointer ": {
|
||||
width: 16,
|
||||
height: 16
|
||||
},
|
||||
".react-colorful__saturation": {
|
||||
mb: 1,
|
||||
borderRadius: "4px"
|
||||
},
|
||||
".react-colorful__last-control": {
|
||||
borderRadius: "4px"
|
||||
},
|
||||
".react-colorful__hue-pointer": {
|
||||
width: "4px",
|
||||
borderRadius: "4px",
|
||||
height: 24,
|
||||
cursor: "col-resize"
|
||||
},
|
||||
".react-colorful__saturation-pointer": {
|
||||
cursor: "all-scroll"
|
||||
},
|
||||
input: {
|
||||
padding: 1,
|
||||
border: "1px solid",
|
||||
borderColor: "grey.300",
|
||||
borderRadius: "4px",
|
||||
width: "100%"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function Picker({ value, onChange }: Props) {
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
const handleChange = (v: string) => {
|
||||
setInternalValue(v);
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
|
||||
onChange(v);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={1} sx={SX}>
|
||||
<HexColorPicker color={value} onChange={handleChange} />
|
||||
<Swatch
|
||||
paletteColors={DEFAULT_PRESET_COLORS}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Box pt={1}>
|
||||
<HexColorInput prefixed color={internalValue} onChange={handleChange} />
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Button, SxProps } from "@mui/material";
|
||||
|
||||
type Props = {
|
||||
paletteColors: string[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const TILE_BUTTON: SxProps = {
|
||||
width: 24,
|
||||
height: 24
|
||||
};
|
||||
export default function Swatch({ paletteColors, value, onChange }: Props) {
|
||||
const renderButton = (colorValue: string) => {
|
||||
return (
|
||||
<Button
|
||||
key={colorValue}
|
||||
onClick={() => onChange(colorValue)}
|
||||
sx={{
|
||||
...TILE_BUTTON,
|
||||
backgroundColor: colorValue,
|
||||
border: "1px solid",
|
||||
borderColor: value === colorValue ? "black" : "grey.200",
|
||||
minWidth: 24,
|
||||
display: "inline-flex",
|
||||
"&:hover": {
|
||||
backgroundColor: colorValue,
|
||||
borderColor: "grey.500"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
sx={{
|
||||
display: "grid",
|
||||
gap: 1,
|
||||
gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr"
|
||||
}}
|
||||
>
|
||||
{paletteColors.map((c) => renderButton(c))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
import BaseColorInput from "./BaseColorInput";
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultValue: string;
|
||||
};
|
||||
export default function ColorInput(props: Props) {
|
||||
return <BaseColorInput {...props} nullable={false} />;
|
||||
}
|
||||
|
||||
type NullableProps = {
|
||||
label: string;
|
||||
onChange: (value: null | string) => void;
|
||||
defaultValue: null | string;
|
||||
};
|
||||
export function NullableColorInput(props: NullableProps) {
|
||||
return <BaseColorInput {...props} nullable />;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Stack } from '@mui/material';
|
||||
|
||||
import TextDimensionInput from './TextDimensionInput';
|
||||
|
||||
export const DEFAULT_2_COLUMNS = [6] as [number];
|
||||
export const DEFAULT_3_COLUMNS = [4, 8] as [number, number];
|
||||
|
||||
type TWidthValue = number | null | undefined;
|
||||
type FixedWidths = [
|
||||
//
|
||||
number | null | undefined,
|
||||
number | null | undefined,
|
||||
number | null | undefined,
|
||||
];
|
||||
type ColumnsLayoutInputProps = {
|
||||
defaultValue: FixedWidths | null | undefined;
|
||||
onChange: (v: FixedWidths | null | undefined) => void;
|
||||
};
|
||||
export default function ColumnWidthsInput({ defaultValue, onChange }: ColumnsLayoutInputProps) {
|
||||
const [currentValue, setCurrentValue] = useState<[TWidthValue, TWidthValue, TWidthValue]>(() => {
|
||||
if (defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
return [null, null, null];
|
||||
});
|
||||
|
||||
const setIndexValue = (index: 0 | 1 | 2, value: number | null | undefined) => {
|
||||
const nValue: FixedWidths = [...currentValue];
|
||||
nValue[index] = value;
|
||||
setCurrentValue(nValue);
|
||||
onChange(nValue);
|
||||
};
|
||||
|
||||
const columnsCountValue = 3;
|
||||
let column3 = null;
|
||||
if (columnsCountValue === 3) {
|
||||
column3 = (
|
||||
<TextDimensionInput
|
||||
label="Column 3"
|
||||
defaultValue={currentValue?.[2]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(2, v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<TextDimensionInput
|
||||
label="Column 1"
|
||||
defaultValue={currentValue?.[0]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(0, v);
|
||||
}}
|
||||
/>
|
||||
<TextDimensionInput
|
||||
label="Column 2"
|
||||
defaultValue={currentValue?.[1]}
|
||||
onChange={(v) => {
|
||||
setIndexValue(1, v);
|
||||
}}
|
||||
/>
|
||||
{column3}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { MenuItem, TextField } from '@mui/material';
|
||||
|
||||
import { FONT_FAMILIES } from '../../../../../../documents/blocks/helpers/fontFamily';
|
||||
|
||||
const OPTIONS = FONT_FAMILIES.map((option) => (
|
||||
<MenuItem key={option.key} value={option.key} sx={{ fontFamily: option.value }}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
type NullableProps = {
|
||||
label: string;
|
||||
onChange: (value: null | string) => void;
|
||||
defaultValue: null | string;
|
||||
};
|
||||
export function NullableFontFamily({ label, onChange, defaultValue }: NullableProps) {
|
||||
const [value, setValue] = useState(defaultValue ?? 'inherit');
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
variant="standard"
|
||||
label={label}
|
||||
value={value}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value;
|
||||
setValue(v);
|
||||
onChange(v === null ? null : v);
|
||||
}}
|
||||
>
|
||||
<MenuItem value="inherit">Match email settings</MenuItem>
|
||||
{OPTIONS}
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { TextFieldsOutlined } from '@mui/icons-material';
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: number;
|
||||
onChange: (v: number) => void;
|
||||
};
|
||||
export default function FontSizeInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const handleChange = (value: number) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
};
|
||||
return (
|
||||
<Stack spacing={1} alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<RawSliderInput
|
||||
iconLabel={<TextFieldsOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value}
|
||||
setValue={handleChange}
|
||||
units="px"
|
||||
step={1}
|
||||
min={10}
|
||||
max={48}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ToggleButton } from '@mui/material';
|
||||
|
||||
import RadioGroupInput from './RadioGroupInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
export default function FontWeightInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<RadioGroupInput
|
||||
label={label}
|
||||
defaultValue={value}
|
||||
onChange={(fontWeight) => {
|
||||
setValue(fontWeight);
|
||||
onChange(fontWeight);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="normal">Regular</ToggleButton>
|
||||
<ToggleButton value="bold">Bold</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
AlignHorizontalLeftOutlined,
|
||||
AlignHorizontalRightOutlined,
|
||||
AlignVerticalBottomOutlined,
|
||||
AlignVerticalTopOutlined,
|
||||
} from '@mui/icons-material';
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type TPaddingValue = {
|
||||
top: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
left: number;
|
||||
};
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: TPaddingValue | null;
|
||||
onChange: (value: TPaddingValue) => void;
|
||||
};
|
||||
export default function PaddingInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(() => {
|
||||
if (defaultValue) {
|
||||
return defaultValue;
|
||||
}
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
};
|
||||
});
|
||||
|
||||
function handleChange(internalName: keyof TPaddingValue, nValue: number) {
|
||||
const v = {
|
||||
...value,
|
||||
[internalName]: nValue,
|
||||
};
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={2} alignItems="flex-start" pb={1}>
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignVerticalTopOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.top}
|
||||
setValue={(num) => handleChange('top', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignVerticalBottomOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.bottom}
|
||||
setValue={(num) => handleChange('bottom', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignHorizontalLeftOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.left}
|
||||
setValue={(num) => handleChange('left', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
|
||||
<RawSliderInput
|
||||
iconLabel={<AlignHorizontalRightOutlined sx={{ fontSize: 16 }} />}
|
||||
value={value.right}
|
||||
setValue={(num) => handleChange('right', num)}
|
||||
units="px"
|
||||
step={4}
|
||||
min={0}
|
||||
max={80}
|
||||
marks
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputLabel, Stack, ToggleButtonGroup } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string | JSX.Element;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
defaultValue: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function RadioGroupInput({ label, children, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<Stack alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
fullWidth
|
||||
value={value}
|
||||
size="small"
|
||||
onChange={(_, v: unknown) => {
|
||||
if (typeof v !== 'string') {
|
||||
throw new Error('RadioGroupInput can only receive string values');
|
||||
}
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputLabel, Stack } from '@mui/material';
|
||||
|
||||
import RawSliderInput from './raw/RawSliderInput';
|
||||
|
||||
type SliderInputProps = {
|
||||
label: string;
|
||||
iconLabel: JSX.Element;
|
||||
|
||||
step?: number;
|
||||
marks?: boolean;
|
||||
units: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
defaultValue: number;
|
||||
onChange: (v: number) => void;
|
||||
};
|
||||
|
||||
export default function SliderInput({ label, defaultValue, onChange, ...props }: SliderInputProps) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<Stack spacing={1} alignItems="flex-start">
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<RawSliderInput
|
||||
value={value}
|
||||
setValue={(value: number) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { FormatAlignCenterOutlined, FormatAlignLeftOutlined, FormatAlignRightOutlined } from '@mui/icons-material';
|
||||
import { ToggleButton } from '@mui/material';
|
||||
|
||||
import RadioGroupInput from './RadioGroupInput';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
defaultValue: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
};
|
||||
export default function TextAlignInput({ label, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue ?? 'left');
|
||||
|
||||
return (
|
||||
<RadioGroupInput
|
||||
label={label}
|
||||
defaultValue={value}
|
||||
onChange={(value) => {
|
||||
setValue(value);
|
||||
onChange(value);
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="left">
|
||||
<FormatAlignLeftOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="center">
|
||||
<FormatAlignCenterOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="right">
|
||||
<FormatAlignRightOutlined fontSize="small" />
|
||||
</ToggleButton>
|
||||
</RadioGroupInput>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TextField, Typography } from '@mui/material';
|
||||
|
||||
type TextDimensionInputProps = {
|
||||
label: string;
|
||||
defaultValue: number | null | undefined;
|
||||
onChange: (v: number | null) => void;
|
||||
};
|
||||
export default function TextDimensionInput({ label, defaultValue, onChange }: TextDimensionInputProps) {
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = parseInt(ev.target.value);
|
||||
onChange(isNaN(value) ? null : value);
|
||||
};
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
onChange={handleChange}
|
||||
defaultValue={defaultValue}
|
||||
label={label}
|
||||
variant="standard"
|
||||
placeholder="auto"
|
||||
size="small"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
px
|
||||
</Typography>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { InputProps, TextField } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
helperText?: string | JSX.Element;
|
||||
InputProps?: InputProps;
|
||||
defaultValue: string;
|
||||
onChange: (v: string) => void;
|
||||
};
|
||||
export default function TextInput({ helperText, label, placeholder, rows, InputProps, defaultValue, onChange }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const isMultiline = typeof rows === 'number' && rows > 1;
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline={isMultiline}
|
||||
minRows={rows}
|
||||
variant={isMultiline ? 'outlined' : 'standard'}
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
helperText={helperText}
|
||||
InputProps={InputProps}
|
||||
value={value}
|
||||
onChange={(ev) => {
|
||||
const v = ev.target.value;
|
||||
setValue(v);
|
||||
onChange(v);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Slider, Stack, Typography } from "@mui/material";
|
||||
|
||||
type SliderInputProps = {
|
||||
iconLabel: JSX.Element;
|
||||
|
||||
step?: number;
|
||||
marks?: boolean;
|
||||
units: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
value: number;
|
||||
setValue: (v: number) => void;
|
||||
};
|
||||
|
||||
export default function RawSliderInput({
|
||||
iconLabel,
|
||||
value,
|
||||
setValue,
|
||||
units,
|
||||
...props
|
||||
}: SliderInputProps) {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Box sx={{ minWidth: 24, lineHeight: 1, flexShrink: 0 }}>{iconLabel}</Box>
|
||||
<Slider
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={(_, value: unknown) => {
|
||||
if (typeof value !== "number") {
|
||||
throw new Error(
|
||||
"RawSliderInput values can only receive numeric values"
|
||||
);
|
||||
}
|
||||
setValue(value);
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ minWidth: 32, textAlign: "right", flexShrink: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{value}
|
||||
{units}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
|
||||
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
|
||||
|
||||
import SingleStylePropertyPanel from "./SingleStylePropertyPanel";
|
||||
|
||||
type MultiStylePropertyPanelProps = {
|
||||
names: (keyof TStyle)[];
|
||||
value: TStyle | undefined | null;
|
||||
onChange: (style: TStyle) => void;
|
||||
};
|
||||
export default function MultiStylePropertyPanel({
|
||||
names,
|
||||
value,
|
||||
onChange
|
||||
}: MultiStylePropertyPanelProps) {
|
||||
return (
|
||||
<>
|
||||
{names.map((name) => (
|
||||
<SingleStylePropertyPanel
|
||||
key={name}
|
||||
name={name}
|
||||
value={value || {}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
|
||||
import { RoundedCornerOutlined } from "@mui/icons-material";
|
||||
|
||||
import { TStyle } from "../../../../../../documents/blocks/helpers/TStyle";
|
||||
import { NullableColorInput } from "../inputs/ColorInput";
|
||||
import { NullableFontFamily } from "../inputs/FontFamily";
|
||||
import FontSizeInput from "../inputs/FontSizeInput";
|
||||
import FontWeightInput from "../inputs/FontWeightInput";
|
||||
import PaddingInput from "../inputs/PaddingInput";
|
||||
import SliderInput from "../inputs/SliderInput";
|
||||
import TextAlignInput from "../inputs/TextAlignInput";
|
||||
|
||||
type StylePropertyPanelProps = {
|
||||
name: keyof TStyle;
|
||||
value: TStyle;
|
||||
onChange: (style: TStyle) => void;
|
||||
};
|
||||
export default function SingleStylePropertyPanel({
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
}: StylePropertyPanelProps) {
|
||||
const defaultValue = value[name] ?? null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleChange = (v: any) => {
|
||||
onChange({ ...value, [name]: v });
|
||||
};
|
||||
|
||||
switch (name) {
|
||||
case "backgroundColor":
|
||||
return (
|
||||
<NullableColorInput
|
||||
label="Background color"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "borderColor":
|
||||
return (
|
||||
<NullableColorInput
|
||||
label="Border color"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "borderRadius":
|
||||
return (
|
||||
<SliderInput
|
||||
iconLabel={<RoundedCornerOutlined />}
|
||||
units="px"
|
||||
step={4}
|
||||
marks
|
||||
min={0}
|
||||
max={48}
|
||||
label="Border radius"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "color":
|
||||
return (
|
||||
<NullableColorInput
|
||||
label="Text color"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "fontFamily":
|
||||
return (
|
||||
<NullableFontFamily
|
||||
label="Font family"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "fontSize":
|
||||
return (
|
||||
<FontSizeInput
|
||||
label="Font size"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "fontWeight":
|
||||
return (
|
||||
<FontWeightInput
|
||||
label="Font weight"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "textAlign":
|
||||
return (
|
||||
<TextAlignInput
|
||||
label="Alignment"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "padding":
|
||||
return (
|
||||
<PaddingInput
|
||||
label="Padding"
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
import { setDocument, useDocument } from "../../documents/editor/EditorContext";
|
||||
|
||||
import EmailLayoutSidebarPanel from "./ConfigurationPanel/input-panels/EmailLayoutSidebarPanel";
|
||||
|
||||
export default function StylesPanel() {
|
||||
const block = useDocument().root;
|
||||
if (!block) {
|
||||
return <p>Block not found</p>;
|
||||
}
|
||||
|
||||
const { data, type } = block;
|
||||
if (type !== "EmailLayout") {
|
||||
throw new Error('Expected "root" element to be of type EmailLayout');
|
||||
}
|
||||
|
||||
return (
|
||||
<EmailLayoutSidebarPanel
|
||||
key="root"
|
||||
data={data}
|
||||
setData={(data) => setDocument({ root: { type, data } })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
|
||||
import { AppRegistrationOutlined, LastPageOutlined } from "@mui/icons-material";
|
||||
import { IconButton } from "@mui/material";
|
||||
|
||||
import {
|
||||
toggleInspectorDrawerOpen,
|
||||
useInspectorDrawerOpen
|
||||
} from "../../documents/editor/EditorContext";
|
||||
|
||||
export default function ToggleInspectorPanelButton() {
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
|
||||
const handleClick = () => {
|
||||
toggleInspectorDrawerOpen();
|
||||
};
|
||||
if (inspectorDrawerOpen) {
|
||||
return (
|
||||
<IconButton onClick={handleClick}>
|
||||
<LastPageOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IconButton onClick={handleClick}>
|
||||
<AppRegistrationOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Drawer, Tab, Tabs } from "@mui/material";
|
||||
|
||||
import {
|
||||
setSidebarTab,
|
||||
useInspectorDrawerOpen,
|
||||
useSelectedSidebarTab
|
||||
} from "../../documents/editor/EditorContext";
|
||||
|
||||
import ConfigurationPanel from "./ConfigurationPanel";
|
||||
import StylesPanel from "./StylesPanel";
|
||||
|
||||
export const INSPECTOR_DRAWER_WIDTH = 320;
|
||||
|
||||
export default function InspectorDrawer() {
|
||||
const selectedSidebarTab = useSelectedSidebarTab();
|
||||
const inspectorDrawerOpen = useInspectorDrawerOpen();
|
||||
|
||||
const renderCurrentSidebarPanel = () => {
|
||||
switch (selectedSidebarTab) {
|
||||
case "block-configuration":
|
||||
return <ConfigurationPanel />;
|
||||
case "styles":
|
||||
return <StylesPanel />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
anchor="right"
|
||||
open={inspectorDrawerOpen}
|
||||
sx={{
|
||||
width: inspectorDrawerOpen ? INSPECTOR_DRAWER_WIDTH : 0
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: INSPECTOR_DRAWER_WIDTH,
|
||||
height: 49,
|
||||
borderBottom: 1,
|
||||
borderColor: "divider"
|
||||
}}
|
||||
>
|
||||
<Box px={2}>
|
||||
<Tabs
|
||||
value={selectedSidebarTab}
|
||||
onChange={(_, v) => setSidebarTab(v)}
|
||||
>
|
||||
<Tab value="styles" label="Styles" />
|
||||
<Tab value="block-configuration" label="Inspect" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: INSPECTOR_DRAWER_WIDTH,
|
||||
height: "calc(100% - 49px)",
|
||||
overflow: "auto"
|
||||
}}
|
||||
>
|
||||
{renderCurrentSidebarPanel()}
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||