chore: migrate tx-builder from safe-apps repo to this monorepo (#6953)
* chore: migrate tx-builder from safe-apps repo to this monorepo create-react-app is deprecated and I’ve migrated the code to vite. web3 is deprecated and I’ve moved the code to ethersjs the cypress tests were broken and I dropped them original code used mui4 -> here we’ve migrated to mui6 * fix(tx-builder): remove build from tx-builder * fix(tx-builder): vite config * fix(tx-builder): linting * fix(tx-builder): fix typo * fix(tx-builder): don’t mutate array * fix(tx-builder): fix exponential backtracking * fix(tx-builder): git workflows * fix(tx-builder): fix lint setup * fix(tx-builder): align package dependencies * fix(tx-builder): invalid react-dom version * fix(tx-builder): git workflow * fix(tx-builder): drag and drop was not working react-beautiful-dnd was no longer supported and it was not compatible with react 19. Moved to @hello-pangea/dnd * fix(tx-builder): fix pallet issues
@@ -0,0 +1,85 @@
|
||||
name: tx-builder Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- apps/tx-builder/**
|
||||
- packages/**
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths:
|
||||
- apps/tx-builder/**
|
||||
- packages/**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/yarn
|
||||
- name: Run ESLint
|
||||
run: yarn workspace @safe-global/tx-builder lint
|
||||
|
||||
prettier:
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/yarn
|
||||
- name: Run Prettier check
|
||||
run: yarn workspace @safe-global/tx-builder prettier
|
||||
|
||||
type-check:
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/yarn
|
||||
- name: Type check
|
||||
run: yarn workspace @safe-global/tx-builder type-check
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/yarn
|
||||
- name: Run unit tests
|
||||
run: yarn workspace @safe-global/tx-builder test --ci --coverage
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: tx-builder
|
||||
fail_ci_if_error: false
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/yarn
|
||||
- name: Build
|
||||
run: yarn workspace @safe-global/tx-builder build
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tx-builder-build
|
||||
path: apps/tx-builder/build
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,228 @@
|
||||
name: tx-builder Deploy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- apps/tx-builder/**
|
||||
- packages/**
|
||||
- .github/workflows/tx-builder-deploy.yml
|
||||
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- apps/tx-builder/**
|
||||
- packages/**
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
description: 'Create production release'
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: apps/tx-builder
|
||||
|
||||
jobs:
|
||||
pr-preview:
|
||||
name: PR Preview
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Post building comment
|
||||
uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
message-id: tx-builder-preview
|
||||
message: |
|
||||
## tx-builder Preview
|
||||
⏳ Building preview deployment...
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: ./.github/actions/yarn
|
||||
|
||||
- name: Build
|
||||
run: yarn workspace @safe-global/tx-builder build
|
||||
env:
|
||||
VITE_TENDERLY_ORG_NAME: ${{ secrets.NEXT_PUBLIC_TENDERLY_ORG_NAME }}
|
||||
VITE_TENDERLY_PROJECT_NAME: ${{ secrets.NEXT_PUBLIC_TENDERLY_PROJECT_NAME }}
|
||||
VITE_TENDERLY_SIMULATE_ENDPOINT_URL: ${{ secrets.NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL }}
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract_branch
|
||||
shell: bash
|
||||
run: echo "branch=$(echo $GITHUB_HEAD_REF | sed 's/[^a-z0-9]/_/ig' | sed 's/[A-Z]/\L&/g')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Deploy to S3
|
||||
env:
|
||||
BUCKET: s3://${{ secrets.AWS_REVIEW_BUCKET_NAME }}/tx-builder/${{ steps.extract_branch.outputs.branch }}
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
run: aws s3 sync ./build $BUCKET --delete
|
||||
|
||||
- name: Post deployment link
|
||||
if: always()
|
||||
uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
message-id: tx-builder-preview
|
||||
message: |
|
||||
## tx-builder Preview
|
||||
✅ Deploy successful!
|
||||
|
||||
**Preview URL:**
|
||||
https://${{ steps.extract_branch.outputs.branch }}--tx-builder.review.5afe.dev/tx-builder/
|
||||
message-failure: |
|
||||
## tx-builder Preview
|
||||
❌ Deploy failed!
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
deploy-staging:
|
||||
name: Deploy Staging
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: ./.github/actions/yarn
|
||||
|
||||
- name: Build
|
||||
run: yarn workspace @safe-global/tx-builder build
|
||||
env:
|
||||
VITE_TENDERLY_ORG_NAME: ${{ secrets.NEXT_PUBLIC_TENDERLY_ORG_NAME }}
|
||||
VITE_TENDERLY_PROJECT_NAME: ${{ secrets.NEXT_PUBLIC_TENDERLY_PROJECT_NAME }}
|
||||
VITE_TENDERLY_SIMULATE_ENDPOINT_URL: ${{ secrets.NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL }}
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Deploy to staging
|
||||
env:
|
||||
BUCKET: s3://${{ secrets.AWS_STAGING_BUCKET_NAME }}/current/tx-builder
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
run: aws s3 sync ./build $BUCKET --delete
|
||||
|
||||
production-release:
|
||||
name: Production Release
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
env:
|
||||
PROD_DEPLOYMENT_HOOK_URL: ${{ secrets.PROD_DEPLOYMENT_HOOK_URL }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/yarn
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
run: |
|
||||
VERSION=$(node -p 'require("./package.json").version')
|
||||
echo "version=tx-builder-v$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version_number=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "📦 Version: $VERSION"
|
||||
|
||||
- name: Check if tag already exists
|
||||
run: |
|
||||
if git rev-parse "${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
|
||||
echo "❌ Tag ${{ steps.version.outputs.version }} already exists!"
|
||||
echo "Make sure you've bumped the version in apps/tx-builder/package.json"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Tag is new"
|
||||
|
||||
- name: Build
|
||||
run: yarn workspace @safe-global/tx-builder build
|
||||
env:
|
||||
VITE_TENDERLY_ORG_NAME: ${{ secrets.NEXT_PUBLIC_TENDERLY_ORG_NAME }}
|
||||
VITE_TENDERLY_PROJECT_NAME: ${{ secrets.NEXT_PUBLIC_TENDERLY_PROJECT_NAME }}
|
||||
VITE_TENDERLY_SIMULATE_ENDPOINT_URL: ${{ secrets.NEXT_PUBLIC_TENDERLY_SIMULATE_ENDPOINT_URL }}
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Deploy to production S3
|
||||
env:
|
||||
BUCKET: s3://${{ secrets.AWS_STAGING_BUCKET_NAME }}/releases/${{ steps.version.outputs.version }}
|
||||
working-directory: ${{ env.WORKING_DIRECTORY }}
|
||||
run: aws s3 sync ./build $BUCKET --delete
|
||||
|
||||
- name: Create git tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag ${{ steps.version.outputs.version }}
|
||||
git push origin ${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: tx-builder ${{ steps.version.outputs.version_number }}
|
||||
tag_name: ${{ steps.version.outputs.version }}
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
## tx-builder v${{ steps.version.outputs.version_number }}
|
||||
|
||||
Release of tx-builder Safe App.
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Trigger production deployment
|
||||
if: env.PROD_DEPLOYMENT_HOOK_URL != ''
|
||||
env:
|
||||
PROD_DEPLOYMENT_HOOK_TOKEN: ${{ secrets.PROD_DEPLOYMENT_HOOK_TOKEN }}
|
||||
VERSION_TAG: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
HTTP_STATUS=$(curl --silent --show-error --output /dev/null --write-out "%{http_code}" -X POST \
|
||||
-F token="$PROD_DEPLOYMENT_HOOK_TOKEN" \
|
||||
-F ref=master \
|
||||
-F "variables[TRIGGER_RELEASE_COMMIT_TAG]=$VERSION_TAG" \
|
||||
$PROD_DEPLOYMENT_HOOK_URL)
|
||||
if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then
|
||||
echo "❌ Production deployment trigger failed (HTTP $HTTP_STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### 🎉 tx-builder Release Complete!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Tag:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "The release has been:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Built and uploaded to S3" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ Tagged in git" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- ✅ GitHub release created" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -103,3 +103,7 @@ apps/web/src/types/contracts/*
|
||||
|
||||
# expo-plugins/notifications-service-ios
|
||||
/expo-plugins/notification-service-ios/dist/*
|
||||
|
||||
|
||||
# tx-builder
|
||||
apps/tx-builder/build/*
|
||||
@@ -1,195 +1,173 @@
|
||||
<!--
|
||||
SYNC IMPACT REPORT
|
||||
==================
|
||||
Version Change: N/A → 1.0.0 (Initial constitution)
|
||||
Modified Principles: N/A (Initial creation)
|
||||
Added Sections:
|
||||
- Core Principles (5 principles established)
|
||||
- Architecture Constraints
|
||||
- Quality Standards
|
||||
- Governance
|
||||
Removed Sections: N/A (Initial creation)
|
||||
Templates Status:
|
||||
✅ plan-template.md - Constitution Check section aligns with principles
|
||||
✅ spec-template.md - Scope/requirements align with testing and quality principles
|
||||
✅ tasks-template.md - Task categorization reflects principle-driven task types
|
||||
⚠ Command files - Generic guidance ready, no CLAUDE-specific references
|
||||
Follow-up TODOs:
|
||||
- RATIFICATION_DATE needs to be set (currently marked TODO)
|
||||
- Future reviews should validate compliance with monorepo-specific constraints
|
||||
Sync Impact Report
|
||||
==================
|
||||
Version change: 1.0.0 (initial)
|
||||
Modified principles: N/A (initial constitution)
|
||||
Added sections:
|
||||
- I. Type Safety (NON-NEGOTIABLE)
|
||||
- II. Branch Protection & Quality Gates
|
||||
- III. Cross-Platform Consistency
|
||||
- IV. Testing Discipline
|
||||
- V. Feature Organization
|
||||
- VI. Theme System Integrity
|
||||
- Technology Stack section
|
||||
- Development Workflow section
|
||||
Templates requiring updates:
|
||||
- .specify/templates/plan-template.md ✅ (Constitution Check section compatible)
|
||||
- .specify/templates/spec-template.md ✅ (no changes required)
|
||||
- .specify/templates/tasks-template.md ✅ (no changes required)
|
||||
Follow-up TODOs: None
|
||||
-->
|
||||
|
||||
# Safe{Wallet} Constitution
|
||||
# Safe{Wallet} Monorepo Constitution
|
||||
|
||||
## Core Principles
|
||||
|
||||
### I. Monorepo Unity
|
||||
### I. Type Safety (NON-NEGOTIABLE)
|
||||
|
||||
The Safe{Wallet} monorepo MUST maintain unified standards and shared infrastructure across web and mobile platforms.
|
||||
TypeScript's type system is the first line of defense against bugs. The `any` type is
|
||||
strictly forbidden across the entire codebase.
|
||||
|
||||
**Rules:**
|
||||
- **MUST** never use `any` type - create proper interfaces/types instead
|
||||
- **MUST** use `as` type assertions only when TypeScript cannot infer correctly, never to bypass type errors
|
||||
- **MUST** use Zod for runtime validation at system boundaries (API responses, user input)
|
||||
- **MUST** prefer interfaces over type aliases for object shapes
|
||||
- Test helpers MUST be properly typed - no `as any` escapes in tests
|
||||
|
||||
- Shared packages (`packages/**`) MUST work for both web and mobile environments
|
||||
- Environment variables MUST follow dual-prefix patterns (`NEXT_PUBLIC_*` || `EXPO_PUBLIC_*`) in shared code
|
||||
- Theme tokens MUST be defined in `@safe-global/theme` as the single source of truth
|
||||
- Breaking changes to shared packages MUST be validated against both platforms before merging
|
||||
- Redux store state changes MUST be compatible with both web and mobile consumers
|
||||
**Rationale**: A single `any` can cascade through the codebase, silently breaking type guarantees
|
||||
and allowing runtime errors that TypeScript was designed to prevent.
|
||||
|
||||
**Rationale:** The monorepo architecture enables code reuse and consistency. Violations create platform-specific bugs, deployment blockers, and maintenance overhead that compound over time.
|
||||
### II. Branch Protection & Quality Gates
|
||||
|
||||
### II. Type Safety
|
||||
All changes flow through pull requests. Direct pushes to protected branches are forbidden.
|
||||
|
||||
TypeScript's type system MUST be strictly enforced without exceptions.
|
||||
- **MUST** never push directly to `dev` (default) or `main` (production) branches
|
||||
- **MUST** create feature branches for all changes: `feature/your-feature-name`
|
||||
- **MUST** pass all quality gates before committing:
|
||||
- `yarn workspace @safe-global/web type-check` (or mobile)
|
||||
- `yarn workspace @safe-global/web lint`
|
||||
- `yarn workspace @safe-global/web prettier`
|
||||
- `yarn workspace @safe-global/web test`
|
||||
- **MUST** use semantic commit messages: `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`
|
||||
- **MUST** never commit failing code - all tests must pass
|
||||
|
||||
**Rules:**
|
||||
**Rationale**: Pre-commit hooks enforce these gates, but understanding them prevents wasted cycles
|
||||
debugging hook failures.
|
||||
|
||||
- The `any` type is PROHIBITED in all code (production and tests)
|
||||
- Type assertions (`as any`) are PROHIBITED - create properly typed helpers instead
|
||||
- All functions, variables, and component props MUST have explicit types
|
||||
- Type-check MUST pass before committing: `yarn workspace @safe-global/{web|mobile} type-check`
|
||||
- Generated type files (e.g., `packages/utils/src/types/contracts/`) MUST NOT be manually edited
|
||||
### III. Cross-Platform Consistency
|
||||
|
||||
**Rationale:** Type safety prevents runtime errors, enables refactoring confidence, and provides documentation. The `any` escape hatch creates hidden bugs that surface in production. Properly typed code is self-documenting and maintainable.
|
||||
The monorepo serves both web (Next.js) and mobile (Expo/React Native). Changes to shared code
|
||||
MUST work for both platforms.
|
||||
|
||||
### III. Test-First Development
|
||||
- **MUST** test shared package changes (`packages/**`) against both web and mobile
|
||||
- **MUST** use dual environment variable patterns in shared packages:
|
||||
`process.env.NEXT_PUBLIC_* || process.env.EXPO_PUBLIC_*`
|
||||
- **MUST** ensure Redux store changes work for both platforms
|
||||
- **MUST** never import platform-specific code into shared packages
|
||||
- Web-only features go in `apps/web/`, mobile-only in `apps/mobile/`
|
||||
|
||||
All new logic, services, and hooks MUST be covered by unit tests.
|
||||
**Rationale**: Breaking mobile when changing web (or vice versa) creates silent regressions that
|
||||
surface late in the development cycle.
|
||||
|
||||
**Rules:**
|
||||
### IV. Testing Discipline
|
||||
|
||||
- Business logic MUST have unit tests before merging
|
||||
- Tests MUST verify behavior, not implementation details (avoid checking specific Redux actions dispatched)
|
||||
- Network requests MUST be mocked using Mock Service Worker (MSW), not by mocking `fetch` or ethers.js
|
||||
- Test data MUST be generated using faker for consistency and coverage
|
||||
- Redux tests MUST verify resulting state changes using properly typed helpers
|
||||
- Shared package tests MUST validate behavior for both web and mobile environments
|
||||
- E2E smoke tests (web) MUST pass in CI before deployment
|
||||
Tests validate behavior, not implementation details. Network mocking uses MSW, not function mocks.
|
||||
|
||||
**Rationale:** Tests prevent regressions, document intended behavior, and enable confident refactoring. Implementation-detail tests break during legitimate refactors and provide false confidence.
|
||||
- **MUST** use Mock Service Worker (MSW) for network request mocking, not `jest.mock(fetch)`
|
||||
- **MUST** use MSW for blockchain RPC call mocking, not ethers.js mocks
|
||||
- **MUST** use faker for test data generation
|
||||
- **MUST** colocate test files with source: `Component.tsx` → `Component.test.tsx`
|
||||
- **MUST** verify Redux state changes, not action dispatch calls
|
||||
- **MUST** cover new logic, services, and hooks with unit tests
|
||||
|
||||
### IV. Design System Compliance
|
||||
**Rationale**: Mocking implementation details (function calls) creates brittle tests that break
|
||||
on refactoring. Mocking boundaries (network) validates actual behavior.
|
||||
|
||||
UI implementation MUST follow established design system patterns and theme tokens.
|
||||
### V. Feature Organization
|
||||
|
||||
**Rules:**
|
||||
Features are modular, isolated, and controlled by feature flags.
|
||||
|
||||
- **Web:** MUST use MUI components with Safe MUI theme; MUST use theme variables from `vars.css`
|
||||
- **Mobile:** MUST use Tamagui components; MUST use theme tokens from `@safe-global/theme`
|
||||
- Hard-coded colors, spacing, or typography values are PROHIBITED
|
||||
- `apps/web/src/styles/vars.css` MUST NOT be manually edited (auto-generated)
|
||||
- New components MUST have corresponding Storybook stories (`.stories.tsx`) for web
|
||||
- Component stories MUST document all important states and variations
|
||||
- Light and dark mode theming MUST be updated together for consistency
|
||||
- **MUST** create new web features in `src/features/[feature-name]/`
|
||||
- **MUST** gate new web features behind feature flags (CGW API chains config)
|
||||
- **MUST** create Storybook stories for new web components (`.stories.tsx`)
|
||||
- Only truly global code belongs in top-level `src/` folders
|
||||
- **MUST** handle loading, error, and empty states in all UI components
|
||||
|
||||
**Rationale:** Consistent design system implementation ensures brand consistency, accessibility, and maintainability. Theme tokens enable global styling changes without code modifications. Storybook provides visual documentation and isolated component development.
|
||||
**Rationale**: Feature flags enable incremental rollout and quick rollback. Feature folders
|
||||
prevent cross-feature coupling and make code ownership clear.
|
||||
|
||||
### V. Safe-Specific Security
|
||||
### VI. Theme System Integrity
|
||||
|
||||
Safe{Wallet} handles multi-signature transactions and must enforce security best practices.
|
||||
The `@safe-global/theme` package is the single source of truth for all design tokens.
|
||||
|
||||
**Rules:**
|
||||
- **MUST** never hardcode colors, spacing, or typography values
|
||||
- **MUST** never edit `apps/web/src/styles/vars.css` directly - it's auto-generated
|
||||
- **MUST** use theme tokens: MUI theme (web) or Tamagui tokens (mobile)
|
||||
- **MUST** update both light and dark mode palettes together for consistency
|
||||
- **MUST** run `yarn workspace @safe-global/web css-vars` after theme changes
|
||||
|
||||
- Private keys or sensitive data MUST NEVER be hardcoded - use environment variables
|
||||
- Ethereum addresses MUST be validated using `ethers.js` utilities (e.g., `isAddress`)
|
||||
- Safe addresses MUST always be referenced with their `chainId` (Safes are chain-specific)
|
||||
- Transaction building MUST use Safe SDK patterns (`@safe-global/protocol-kit`, `@safe-global/api-kit`)
|
||||
- Wallet provider integration MUST follow established Web3-Onboard patterns
|
||||
- Security-critical changes MUST be reviewed by security-knowledgeable maintainers
|
||||
- Multi-signature threshold and owner validation MUST be respected in all transaction flows
|
||||
**Rationale**: Hardcoded values create visual inconsistencies and make theme updates impossible
|
||||
to apply uniformly.
|
||||
|
||||
**Rationale:** Safe{Wallet} is a smart contract wallet managing user assets. Security vulnerabilities can lead to loss of funds. These patterns are established best practices from the Safe ecosystem and MUST be preserved.
|
||||
## Technology Stack
|
||||
|
||||
## Architecture Constraints
|
||||
The monorepo enforces specific technology choices to maintain consistency:
|
||||
|
||||
### Code Organization
|
||||
| Layer | Web (apps/web) | Mobile (apps/mobile) | Shared (packages/) |
|
||||
| --------------- | -------------------- | -------------------- | ------------------ |
|
||||
| Framework | Next.js | Expo (React Native) | Platform-agnostic |
|
||||
| UI Library | MUI | Tamagui | N/A |
|
||||
| State | Redux + RTK Query | Redux + RTK Query | Redux slices |
|
||||
| Styling | CSS vars + MUI theme | Tamagui tokens | @safe-global/theme |
|
||||
| Testing | Jest + MSW + Cypress | Jest + MSW | Jest + MSW |
|
||||
| Package Manager | Yarn 4 workspaces | Yarn 4 workspaces | Yarn 4 workspaces |
|
||||
|
||||
**Web Application:**
|
||||
**Web3/Blockchain stack**:
|
||||
|
||||
- New features MUST be created in `src/features/` with dedicated folders
|
||||
- Only globally-used components, hooks, and services belong in top-level `src/` folders
|
||||
- Each new feature MUST be behind a feature flag (managed via CGW API chain configs)
|
||||
- Safe SDK: `@safe-global/protocol-kit`, `@safe-global/api-kit`
|
||||
- Wallet connection: Web3-Onboard
|
||||
- Ethereum: ethers.js
|
||||
- Address validation: Always use `isAddress` from ethers.js
|
||||
- Chain awareness: Always include `chainId` when referencing a Safe
|
||||
|
||||
**Shared Packages:**
|
||||
## Development Workflow
|
||||
|
||||
- Cross-platform logic MUST reside in `packages/` directory
|
||||
- Shared code MUST NOT import platform-specific dependencies directly
|
||||
- Package changes MUST be validated against both web and mobile consumers
|
||||
Every code change follows this sequence:
|
||||
|
||||
### Dependency Management
|
||||
1. **Branch**: Create feature branch from `dev`
|
||||
2. **Implement**: Write code following platform-specific code style guides
|
||||
- Web: `apps/web/docs/code-style.md`
|
||||
- Mobile: `apps/mobile/docs/code-style.md`
|
||||
3. **Validate**: Run quality gates (type-check → lint → prettier → test)
|
||||
4. **Commit**: Use semantic commit messages, ensure hooks pass
|
||||
5. **PR**: Fill out PR template, ensure CI passes
|
||||
6. **Review**: Address feedback, maintain all quality gates
|
||||
|
||||
- Yarn 4 workspaces MUST be used for all dependency management
|
||||
- Dependencies MUST be added via `yarn workspace <workspace-name> add <package>`
|
||||
- Monorepo-wide dependencies belong in root `package.json`
|
||||
- Breaking dependency updates MUST be validated across all affected workspaces
|
||||
**Generated files** - MUST NOT be manually edited:
|
||||
|
||||
### Workflow Enforcement
|
||||
|
||||
- Pre-commit hooks (Husky) MUST run lint-staged (prettier) and type-check
|
||||
- Pre-push hooks MUST run linting before pushing
|
||||
- All checks (type-check, lint, prettier, tests) MUST pass before committing
|
||||
- Commit messages MUST follow semantic commit conventions (feat:, fix:, refactor:, etc.)
|
||||
|
||||
## Quality Standards
|
||||
|
||||
### Code Quality
|
||||
|
||||
**Principles:**
|
||||
|
||||
- Follow DRY (Don't Repeat Yourself) - extract reusable functions, hooks, and components
|
||||
- Prefer functional programming - use pure functions, avoid side effects
|
||||
- Use declarative patterns - React hooks, derived state over manual synchronization
|
||||
- Keep implementations simple - avoid over-engineering and premature abstraction
|
||||
|
||||
**Error Handling:**
|
||||
|
||||
- All UI components MUST handle loading, error, and empty states
|
||||
- Network errors MUST be properly caught and communicated to users
|
||||
- Chain-specific logic MUST handle multi-chain scenarios appropriately
|
||||
|
||||
### Performance
|
||||
|
||||
- Yarn commands MUST be scoped to specific workspaces when possible (e.g., `yarn workspace @safe-global/web dev`)
|
||||
- Build artifacts (`.next`, `node_modules/.cache`) MUST be cleared when troubleshooting build issues
|
||||
- Images and assets MUST be optimized before committing
|
||||
|
||||
### Documentation
|
||||
|
||||
- Storybook stories MUST document component props and usage patterns (web)
|
||||
- Complex business logic MUST include explanatory comments where non-obvious
|
||||
- Code style guidelines MUST be followed:
|
||||
- Web: `apps/web/docs/code-style.md`
|
||||
- Mobile: `apps/mobile/docs/code-style.md`
|
||||
- `packages/utils/src/types/contracts/` - auto-generated from ABIs
|
||||
- `apps/web/src/styles/vars.css` - auto-generated from theme
|
||||
|
||||
## Governance
|
||||
|
||||
### Amendment Process
|
||||
This constitution supersedes informal practices. All PRs and code reviews MUST verify compliance
|
||||
with these principles.
|
||||
|
||||
This constitution supersedes all other development practices and guidelines.
|
||||
**Amendment process**:
|
||||
|
||||
**Amendment Requirements:**
|
||||
1. Propose changes via PR to this file
|
||||
2. Justify why the change is necessary
|
||||
3. Update version according to semantic versioning:
|
||||
- MAJOR: Principle removal or fundamental redefinition
|
||||
- MINOR: New principle or materially expanded guidance
|
||||
- PATCH: Clarifications and typo fixes
|
||||
4. Document migration plan if breaking change
|
||||
|
||||
1. Proposed changes MUST be documented with rationale
|
||||
2. Proposed changes MUST include impact analysis on existing principles
|
||||
3. Amendments MUST be reviewed and approved by repository maintainers
|
||||
4. Version MUST be incremented following semantic versioning:
|
||||
- **MAJOR**: Backward-incompatible governance changes or principle removals
|
||||
- **MINOR**: New principles or materially expanded guidance
|
||||
- **PATCH**: Clarifications, wording improvements, non-semantic refinements
|
||||
5. Dependent templates (plan, spec, tasks, commands) MUST be updated for consistency
|
||||
6. Migration plan MUST be provided for breaking changes
|
||||
**Compliance verification**:
|
||||
|
||||
### Compliance Review
|
||||
- Pre-commit hooks enforce type-check, lint, and formatting
|
||||
- CI enforces full test suite
|
||||
- Code review MUST verify principle adherence
|
||||
- Feature flag requirement verified at PR review
|
||||
|
||||
- All PRs MUST verify compliance with constitutional principles
|
||||
- Complexity that violates principles MUST be explicitly justified in plan documentation
|
||||
- Pre-commit and pre-push hooks enforce automated compliance for type-checking, linting, and formatting
|
||||
- Constitution violations discovered post-merge MUST be addressed with high priority
|
||||
|
||||
### Runtime Guidance
|
||||
|
||||
- AI contributors MUST reference `AGENTS.md` for comprehensive development guidelines
|
||||
- `CLAUDE.md` provides quick-reference instructions for critical workflows
|
||||
- Constitution provides non-negotiable principles; guidance files provide implementation patterns
|
||||
|
||||
**Version**: 1.0.0 | **Ratified**: TODO(RATIFICATION_DATE) | **Last Amended**: 2026-01-08
|
||||
**Version**: 1.0.0 | **Ratified**: 2026-01-12 | **Last Amended**: 2026-01-12
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Transaction Builder
|
||||
|
||||
A Safe App that allows users to compose custom contract interactions and batch them into a single transaction.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- Yarn 4 (via corepack)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# From monorepo root
|
||||
yarn install
|
||||
|
||||
# Start development server
|
||||
yarn workspace @safe-global/tx-builder dev
|
||||
```
|
||||
|
||||
The app will be available at `http://localhost:3000/tx-builder/`
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description |
|
||||
| --------------------------------------------------- | ---------------- |
|
||||
| `yarn workspace @safe-global/tx-builder dev` | Start dev server |
|
||||
| `yarn workspace @safe-global/tx-builder build` | Production build |
|
||||
| `yarn workspace @safe-global/tx-builder test` | Run unit tests |
|
||||
| `yarn workspace @safe-global/tx-builder lint` | Run ESLint |
|
||||
| `yarn workspace @safe-global/tx-builder type-check` | TypeScript check |
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Unit tests use Jest with React Testing Library:
|
||||
|
||||
```bash
|
||||
yarn workspace @safe-global/tx-builder test
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
E2E testing for Safe Apps is handled by the main web app's Cypress suite at `apps/web/cypress/e2e/safe-apps/`. This ensures tests run against the actual Safe{Wallet} integration rather than mocked iframe environments.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Directories
|
||||
|
||||
- `src/components/` - React components
|
||||
- `src/pages/` - Route page components
|
||||
- `src/hooks/` - Custom React hooks
|
||||
- `src/store/` - React Context providers
|
||||
- `src/lib/` - Business logic (batches, simulation, storage)
|
||||
- `src/theme/` - MUI theme configuration
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **React 19** with TypeScript
|
||||
- **MUI v6** for UI components
|
||||
- **ethers.js v6** for Ethereum interactions
|
||||
- **Vite** for bundling
|
||||
- **Jest** + React Testing Library for unit tests
|
||||
|
||||
### Safe Apps Integration
|
||||
|
||||
This app runs as a Safe App inside Safe{Wallet}'s iframe. It uses:
|
||||
|
||||
- `@safe-global/safe-apps-sdk` for communication with Safe{Wallet}
|
||||
- `@safe-global/safe-apps-react-sdk` for React hooks
|
||||
- `@safe-global/safe-apps-provider` for ethers.js provider
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------- | --------------------- | ------- |
|
||||
| `VITE_TENDERLY_SIMULATE_ENDPOINT_URL` | Tenderly API endpoint | - |
|
||||
| `VITE_TENDERLY_PROJECT_NAME` | Tenderly project name | - |
|
||||
| `VITE_TENDERLY_ORG_NAME` | Tenderly organization | - |
|
||||
|
||||
## Deployment
|
||||
|
||||
The app is automatically deployed via GitHub Actions (`.github/workflows/tx-builder-deploy.yml`):
|
||||
|
||||
### Environments
|
||||
|
||||
| Environment | Trigger | URL Pattern |
|
||||
| ----------- | ------------------------ | ---------------------------------------------------------- |
|
||||
| PR Preview | Pull request | `https://{branch}--tx-builder.review.5afe.dev/tx-builder/` |
|
||||
| Staging | Push to `dev` | Staging environment |
|
||||
| Production | Manual workflow dispatch | Versioned release |
|
||||
|
||||
### Production Release Process
|
||||
|
||||
1. **Bump version**: Update the version in `package.json` and merge to `main`:
|
||||
|
||||
```bash
|
||||
# Edit apps/tx-builder/package.json
|
||||
# Change "version": "1.0.0" to "version": "1.1.0"
|
||||
git commit -m "chore: bump tx-builder to 1.1.0"
|
||||
```
|
||||
|
||||
2. **Trigger release**: Go to [GitHub Actions](../../actions/workflows/tx-builder-deploy.yml) and click "Run workflow" (from `main` or `dev` branch)
|
||||
|
||||
3. **Automated steps**: The workflow will:
|
||||
- Build the production bundle from `main`
|
||||
- Upload to S3 releases bucket
|
||||
- Create a git tag (`tx-builder-vX.X.X`)
|
||||
- Create a GitHub release
|
||||
|
||||
4. **Contact DevOps**: Send the git tag link to DevOps for production deployment:
|
||||
```
|
||||
https://github.com/safe-global/safe-wallet-monorepo/releases/tag/tx-builder-vX.X.X
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](../../LICENSE) in the monorepo root.
|
||||
@@ -0,0 +1,95 @@
|
||||
import js from '@eslint/js'
|
||||
import { FlatCompat } from '@eslint/eslintrc'
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals'
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
})
|
||||
|
||||
const typeScriptRules = {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-unnecessary-type-constraint': 'warn',
|
||||
}
|
||||
|
||||
const withTypeScriptRules = (configs) => {
|
||||
let hasTypeScriptPlugin = false
|
||||
|
||||
const updatedConfigs = configs.map((config) => {
|
||||
if (!config.plugins || !config.plugins['@typescript-eslint']) {
|
||||
return config
|
||||
}
|
||||
|
||||
hasTypeScriptPlugin = true
|
||||
|
||||
return {
|
||||
...config,
|
||||
languageOptions: {
|
||||
...config.languageOptions,
|
||||
parser: tsParser,
|
||||
ecmaVersion: config.languageOptions?.ecmaVersion ?? 2020,
|
||||
sourceType: config.languageOptions?.sourceType ?? 'module',
|
||||
parserOptions: {
|
||||
...config.languageOptions?.parserOptions,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...config.rules,
|
||||
...typeScriptRules,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (hasTypeScriptPlugin) {
|
||||
return updatedConfigs
|
||||
}
|
||||
|
||||
return [
|
||||
...updatedConfigs,
|
||||
{
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: typeScriptRules,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/node_modules/', '**/build/', '**/vite.config.ts', '**/jest.config.cjs', '**/src/__mocks__/**'],
|
||||
},
|
||||
...withTypeScriptRules(nextVitals),
|
||||
{
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react/display-name': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react-hooks/incompatible-library': 'off',
|
||||
'react-hooks/refs': 'off',
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/use-memo': 'off',
|
||||
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/tx-builder/tx-builder.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Transaction Builder - Safe App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
testEnvironment: 'jsdom',
|
||||
roots: ['<rootDir>/src'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
|
||||
'\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/src/__mocks__/fileMock.js',
|
||||
'^../../utils/env$': '<rootDir>/src/utils/__mocks__/env.ts',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
tsconfig: {
|
||||
module: 'ESNext',
|
||||
moduleResolution: 'bundler',
|
||||
allowImportingTsExtensions: false,
|
||||
noEmit: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!(@safe-global)/)'],
|
||||
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/main.tsx', '!src/vite-env.d.ts'],
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "@safe-global/tx-builder",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 4000",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prettier": "prettier --check src",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@mui/icons-material": "^6.5.0",
|
||||
"@mui/lab": "^6.0.0-beta.20",
|
||||
"@mui/material": "^6.5.0",
|
||||
"@safe-global/safe-apps-provider": "^0.18.5",
|
||||
"@safe-global/safe-apps-react-sdk": "^4.7.2",
|
||||
"@safe-global/safe-apps-sdk": "^9.1.0",
|
||||
"@safe-global/safe-deployments": "^1.37.49",
|
||||
"@safe-global/safe-gateway-typescript-sdk": "^3.22.9",
|
||||
"axios": "^1.8.2",
|
||||
"ethereum-blockies-base64": "^1.0.2",
|
||||
"ethers": "^6.14.3",
|
||||
"evm-proxy-detection": "^1.0.0",
|
||||
"localforage": "^1.10.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-is": "^19.1.0",
|
||||
"react-media": "^1.10.0",
|
||||
"react-router-dom": "^6.28.2",
|
||||
"react-virtuoso": "^4.12.3",
|
||||
"styled-components": "^5.3.11",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@hookform/devtools": "^4.3.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
||||
"@typescript-eslint/parser": "^8.31.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-config-next": "^16.0.5",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"msw": "^2.7.3",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "~5.9.2",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Transaction Builder",
|
||||
"description": "A Safe app to compose custom transactions",
|
||||
"iconPath": "tx-builder.png",
|
||||
"icons": [
|
||||
{
|
||||
"src": "tx-builder.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,47 @@
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
|
||||
import Header from './components/Header'
|
||||
import CreateTransactions from './pages/CreateTransactions'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import EditTransactionLibrary from './pages/EditTransactionLibrary'
|
||||
import ReviewAndConfirm from './pages/ReviewAndConfirm'
|
||||
import SaveTransactionLibrary from './pages/SaveTransactionLibrary'
|
||||
import TransactionLibrary from './pages/TransactionLibrary'
|
||||
import {
|
||||
HOME_PATH,
|
||||
EDIT_BATCH_PATH,
|
||||
REVIEW_AND_CONFIRM_PATH,
|
||||
SAVE_BATCH_PATH,
|
||||
TRANSACTION_LIBRARY_PATH,
|
||||
} from './routes/routes'
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
{/* App Header */}
|
||||
<Header />
|
||||
|
||||
<Routes>
|
||||
{/* Dashboard Screen (Create transactions) */}
|
||||
<Route path={HOME_PATH} element={<Dashboard />}>
|
||||
{/* Transactions Batch section */}
|
||||
<Route index element={<CreateTransactions />} />
|
||||
|
||||
{/* Save Batch section */}
|
||||
<Route path={SAVE_BATCH_PATH} element={<SaveTransactionLibrary />} />
|
||||
|
||||
{/* Edit Batch section */}
|
||||
<Route path={EDIT_BATCH_PATH} element={<EditTransactionLibrary />} />
|
||||
</Route>
|
||||
|
||||
{/* Review & Confirm Screen */}
|
||||
<Route path={REVIEW_AND_CONFIRM_PATH} element={<ReviewAndConfirm />} />
|
||||
|
||||
{/* Transaction Library Screen */}
|
||||
<Route path={TRANSACTION_LIBRARY_PATH} element={<TransactionLibrary />} />
|
||||
</Routes>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = 'test-file-stub'
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="176" height="117" viewBox="0 0 176 117" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M175.154 110.769C175.42 110.694 175.574 110.418 175.499 110.152C175.424 109.887 175.148 109.732 174.882 109.807L175.154 110.769ZM2.60834 24.5178C2.35502 24.4079 2.06055 24.5241 1.95061 24.7774L0.159115 28.9054C0.0491812 29.1587 0.165414 29.4532 0.41873 29.5632C0.672045 29.6731 0.966519 29.5569 1.07645 29.3035L2.66889 25.6342L6.33825 27.2266C6.59156 27.3366 6.88603 27.2203 6.99597 26.967C7.1059 26.7137 6.98967 26.4192 6.73635 26.3093L2.60834 24.5178ZM1.9442 25.16C2.42519 26.3786 2.9426 27.5793 3.49454 28.761L4.40059 28.3379C3.85714 27.1743 3.34779 25.9923 2.87436 24.7929L1.9442 25.16ZM7.22466 35.6489C8.60309 37.873 10.1067 40.0127 11.7198 42.0601L12.5053 41.4412C10.9148 39.4225 9.43284 37.3136 8.07466 35.1221L7.22466 35.6489ZM16.8909 47.9366C18.7225 49.8056 20.6498 51.5732 22.6569 53.2315L23.2938 52.4607C21.3133 50.8243 19.4119 49.0803 17.6052 47.2367L16.8909 47.9366ZM28.9422 57.8978C31.1268 59.3502 33.3776 60.6819 35.6781 61.8847L36.1415 60.9985C33.8721 59.812 31.6515 58.4981 29.4958 57.065L28.9422 57.8978ZM42.7995 65.132C45.2517 66.0918 47.7392 66.9065 50.2441 67.5667L50.499 66.5997C48.0323 65.9496 45.5813 65.147 43.164 64.2008L42.7995 65.132ZM57.9319 69.0938C60.5476 69.4426 63.1627 69.6153 65.757 69.6014L65.7517 68.6014C63.2052 68.6151 60.6361 68.4456 58.0641 68.1026L57.9319 69.0938ZM73.5776 68.9706C76.1694 68.5573 78.718 67.9373 81.2015 67.0994L80.8819 66.1519C78.4532 66.9713 75.959 67.5783 73.4201 67.9831L73.5776 68.9706ZM88.3929 63.9441C90.6547 62.7009 92.834 61.2428 94.9108 59.5602L94.2813 58.7832C92.251 60.4281 90.1212 61.853 87.9112 63.0678L88.3929 63.9441ZM100.574 54.105C102.241 52.206 103.811 50.1203 105.268 47.8414L104.426 47.3026C102.995 49.5402 101.455 51.5853 99.8222 53.4452L100.574 54.105ZM109.036 40.9716C109.582 39.8035 110.104 38.5965 110.601 37.3501L109.672 36.98C109.182 38.2087 108.668 39.3977 108.13 40.5478L109.036 40.9716ZM110.601 37.3501C111.118 36.053 111.547 34.7639 111.893 33.4873L110.928 33.2253C110.592 34.4647 110.175 35.7177 109.672 36.98L110.601 37.3501ZM113.034 25.4567C113.068 22.6226 112.704 19.9157 112.009 17.3963L111.045 17.6621C111.715 20.0909 112.067 22.7043 112.034 25.4448L113.034 25.4567ZM108.695 9.97258C107.161 7.6079 105.284 5.58035 103.17 3.98556L102.568 4.78385C104.584 6.30495 106.383 8.24505 107.857 10.5168L108.695 9.97258ZM95.7967 0.492533C93.1642 -0.110069 90.3985 -0.115301 87.631 0.5873L87.877 1.55656C90.4861 0.89419 93.0909 0.899001 95.5736 1.46732L95.7967 0.492533ZM80.3331 4.22133C78.3745 5.72715 76.4838 7.66388 74.7098 10.0701L75.5147 10.6635C77.2395 8.32401 79.0661 6.45682 80.9426 5.01412L80.3331 4.22133ZM70.5648 17.0471C70.0023 18.2147 69.4603 19.4489 68.9412 20.7518L69.8702 21.1219C70.3811 19.8396 70.9138 18.6268 71.4657 17.4811L70.5648 17.0471ZM68.9412 20.7518C68.4453 21.9966 67.9866 23.2775 67.5662 24.5903L68.5185 24.8952C68.9321 23.6037 69.3831 22.3445 69.8702 21.1219L68.9412 20.7518ZM65.5748 32.4828C65.0789 35.0992 64.7204 37.7964 64.5061 40.5474L65.5031 40.625C65.7146 37.9096 66.0684 35.2487 66.5573 32.669L65.5748 32.4828ZM64.2967 48.6809C64.3655 51.3678 64.5706 54.0804 64.9182 56.7957L65.9102 56.6687C65.5669 53.9874 65.3643 51.3087 65.2964 48.6553L64.2967 48.6809ZM66.366 64.8045C66.984 67.4332 67.7432 70.0433 68.6493 72.6135L69.5924 72.281C68.6986 69.7458 67.9495 67.1703 67.3395 64.5756L66.366 64.8045ZM71.7864 80.1255C72.9708 82.5563 74.3033 84.9281 75.7894 87.2205L76.6285 86.6765C75.1653 84.4194 73.8526 82.083 72.6854 79.6874L71.7864 80.1255ZM80.6434 93.7559C82.3824 95.806 84.2707 97.7624 86.3132 99.6066L86.9834 98.8644C84.9745 97.0506 83.117 95.1261 81.406 93.109L80.6434 93.7559ZM92.7127 104.65C94.9062 106.153 97.2382 107.542 99.7125 108.802L100.166 107.91C97.7304 106.67 95.4357 105.304 93.278 103.825L92.7127 104.65ZM107.184 112.04C109.666 112.942 112.266 113.727 114.987 114.384L115.222 113.412C112.536 112.763 109.971 111.989 107.526 111.1L107.184 112.04ZM122.956 115.884C125.555 116.243 128.251 116.495 131.045 116.632L131.094 115.633C128.328 115.497 125.662 115.248 123.093 114.893L122.956 115.884ZM139.171 116.709C141.795 116.634 144.497 116.465 147.277 116.197L147.181 115.202C144.422 115.468 141.743 115.635 139.143 115.709L139.171 116.709ZM155.324 115.173C157.916 114.767 160.572 114.279 163.291 113.705L163.084 112.727C160.382 113.297 157.743 113.782 155.169 114.186L155.324 115.173ZM171.172 111.849C172.486 111.508 173.813 111.149 175.154 110.769L174.882 109.807C173.548 110.184 172.227 110.542 170.921 110.881L171.172 111.849Z" fill="#A1A3A7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="156" height="113" viewBox="0 0 156 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.0844 -0.000976562C27.1504 -0.000976562 23.0837 4.08492 23.0837 9.04228V90.4316C23.0837 95.389 27.1504 99.4749 32.0844 99.4749H146.093C151.027 99.4749 155.094 95.389 155.094 90.4316V9.04228C155.094 4.08492 151.027 -0.000976562 146.093 -0.000976562H32.0844ZM31.8558 5.8507H59.4007C59.5056 5.88636 59.6162 5.92202 59.7269 5.94578C61.5563 6.23105 63.071 7.18192 64.6672 8.84587C66.2635 10.5098 67.8365 12.8277 69.5609 15.3118C70.8775 17.2076 72.3165 19.2222 74.0351 21.0645H28.8737V8.89334C28.8737 7.1521 30.1495 5.85054 31.8565 5.85054L31.8558 5.8507ZM70.5611 5.85054H146.304C148.021 5.85054 149.304 7.15205 149.304 8.89334V21.0645H87.0132C86.8257 21.0288 86.6382 21.017 86.4507 21.017C81.4355 20.4643 78.4827 16.6251 75.108 11.7935C73.7194 9.80853 72.2078 7.77017 70.5616 5.85062L70.5611 5.85054ZM36.4006 10.5318C34.8004 10.5318 33.5056 11.8404 33.5056 13.4575C33.5056 15.0747 34.8004 16.3833 36.4006 16.3833C38.0007 16.3833 39.2955 15.0747 39.2955 13.4575C39.2955 11.8404 38.0007 10.5318 36.4006 10.5318ZM46.8224 10.5318C45.2222 10.5318 43.9275 11.8404 43.9275 13.4575C43.9275 15.0747 45.2223 16.3833 46.8224 16.3833C48.4226 16.3833 49.7174 15.0747 49.7174 13.4575C49.7174 11.8404 48.4226 10.5318 46.8224 10.5318ZM57.2443 10.5318C55.6441 10.5318 54.3493 11.8404 54.3493 13.4575C54.3493 15.0747 55.6442 16.3833 57.2443 16.3833C58.8444 16.3833 60.1393 15.0747 60.1393 13.4575C60.1393 11.8404 58.8444 10.5318 57.2443 10.5318ZM28.8737 26.916H148.146V90.5912C148.146 92.3264 146.871 93.6234 145.164 93.6234H31.8555C30.1491 93.6234 28.8737 92.3264 28.8737 90.5912V26.916Z" fill="#E2E3E3"/>
|
||||
<ellipse cx="32.5885" cy="82.3735" rx="31.6825" ry="30.6264" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M81.5068 60.8462C81.5068 60.3856 81.8807 60.0117 82.3413 60.0117L96.1115 60.0117C96.5722 60.0117 96.946 60.3856 96.946 60.8462C96.946 61.3069 96.5722 61.6808 96.1115 61.6808L82.3413 61.6808C81.8807 61.6808 81.5068 61.3069 81.5068 60.8462Z" fill="#B2B5B2"/>
|
||||
<rect x="73.665" y="54.2549" width="31.1222" height="13.1798" rx="6" stroke="#B2B5B2" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M98.9624 79.2203C98.9624 78.7596 99.3363 78.3857 99.7969 78.3857L113.567 78.3857C114.028 78.3857 114.402 78.7596 114.402 79.2203C114.402 79.6809 114.028 80.0548 113.567 80.0548L99.7969 80.0548C99.3363 80.0548 98.9624 79.6809 98.9624 79.2203Z" fill="#B2B5B2"/>
|
||||
<rect x="91.1206" y="72.6289" width="31.1222" height="13.1798" rx="6" stroke="#B2B5B2" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.8884 41.5523C65.8884 41.0916 66.2623 40.7178 66.7229 40.7178L80.4931 40.7178C80.9538 40.7178 81.3276 41.0916 81.3276 41.5523C81.3276 42.013 80.9538 42.3868 80.4931 42.3868L66.7229 42.3868C66.2623 42.3868 65.8884 42.013 65.8884 41.5523Z" fill="#B2B5B2"/>
|
||||
<rect x="58.0466" y="34.9609" width="31.1222" height="13.1798" rx="6" stroke="#B2B5B2" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M62.3045 66.8489C61.2484 66.4265 60.1923 66.9545 59.7699 68.0106L57.2353 74.8751C53.6446 64.4199 43.6118 57.0273 32.3117 57.0273C21.1173 57.133 11.0845 64.2087 7.38818 74.7695C6.96575 75.8256 7.5994 77.0929 8.65548 77.5153C9.71156 77.9378 10.9789 77.3041 11.4013 76.248C14.5695 67.377 22.9126 61.4629 32.3117 61.4629C41.8165 61.4629 50.1595 67.5882 53.2222 76.248L47.0969 74.0303C46.0408 73.6078 44.9847 74.1359 44.5623 75.192C44.3511 75.72 44.4567 76.248 44.6679 76.6705C44.8791 77.0929 45.3015 77.5153 45.724 77.7266L56.8128 81.8453C57.3409 82.0565 57.8689 81.9509 58.2914 81.7397C58.7138 81.5285 59.1362 81.106 59.3474 80.6836L63.4662 69.3835C63.783 68.3274 63.2549 67.2713 62.3045 66.8489Z" fill="#008C73"/>
|
||||
<path d="M56.3906 87.5483C55.2289 87.2315 54.0672 87.9707 53.856 89.0268C51.3214 98.8484 42.4503 105.607 32.4175 105.607C22.2791 105.607 13.3024 98.5316 10.8734 88.8156L17.4211 91.2446C18.4772 91.667 19.5333 91.139 19.9557 90.0829C20.1669 89.5549 20.0613 89.0268 19.8501 88.6044C19.6389 88.182 19.2165 87.7595 18.794 87.5483L7.70515 83.4296C7.17711 83.2184 6.64907 83.324 6.22664 83.5352C5.80421 83.7464 5.38177 84.1688 5.17056 84.5913L1.05183 95.6801C0.629401 96.7362 1.15744 97.7923 2.21353 98.2147C3.26961 98.6372 4.32569 98.1091 4.74812 97.053L7.07151 90.8222C10.3454 101.911 20.695 109.937 32.4175 109.937C44.4568 109.937 55.0177 101.805 57.9747 90.1885C58.1859 89.0268 57.4466 87.8651 56.3906 87.5483Z" fill="#008C73"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.6519 83.5357C23.6519 88.2881 27.4538 92.09 32.2061 92.09C36.9585 92.09 40.7604 88.2881 40.7604 83.5357C40.7604 78.7833 36.9585 74.9814 32.2061 74.9814C27.4538 74.9814 23.6519 78.7833 23.6519 83.5357ZM27.8763 83.4305C27.8763 81.1072 29.7772 79.1006 32.2062 79.1006C34.5296 79.1006 36.5361 81.1072 36.5361 83.4305C36.5361 85.7539 34.6352 87.7605 32.2062 87.7605C29.8828 87.7605 27.8763 85.8595 27.8763 83.4305Z" fill="#008C73"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,82 @@
|
||||
import { ReactElement } from 'react'
|
||||
import AccordionMUI, { AccordionProps as AccordionMUIProps } from '@mui/material/Accordion'
|
||||
import AccordionSummaryMUI, { AccordionSummaryProps as AccordionSummaryMUIProps } from '@mui/material/AccordionSummary'
|
||||
import styled from 'styled-components'
|
||||
import FixedIcon from '../FixedIcon'
|
||||
|
||||
type AccordionProps = AccordionMUIProps & {
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
type StyledAccordionProps = AccordionMUIProps & {
|
||||
$compact?: AccordionProps['compact']
|
||||
}
|
||||
|
||||
const StyledAccordion = styled(AccordionMUI)<StyledAccordionProps>`
|
||||
&.MuiAccordion-root {
|
||||
border-radius: ${({ $compact }) => ($compact ? '8px' : '0')};
|
||||
border: ${({ $compact, theme }) => ($compact ? '2px solid ' + theme.palette.divider : 'none')};
|
||||
border-bottom: 2px solid ${({ theme }) => theme.palette.divider};
|
||||
margin-bottom: ${({ $compact }) => ($compact ? '16px' : '0')};
|
||||
overflow: hidden;
|
||||
|
||||
&:before {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: 2px solid ${({ theme }) => theme.palette.divider};
|
||||
}
|
||||
|
||||
&.Mui-expanded {
|
||||
margin: ${({ $compact }) => ($compact ? '0 0 16px 0' : '0')};
|
||||
}
|
||||
|
||||
.MuiAccordionDetails-root {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummaryMUI)`
|
||||
&.MuiAccordionSummary-root {
|
||||
&.Mui-expanded {
|
||||
min-height: 48px;
|
||||
border-bottom: 2px solid ${({ theme }) => theme.palette.divider};
|
||||
background-color: ${({ theme }) => theme.palette.background.default};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.palette.background.default};
|
||||
}
|
||||
|
||||
.MuiAccordionSummary-content {
|
||||
&.Mui-expanded {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.MuiIconButton-root {
|
||||
font-size: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Accordion = ({ compact, children, ...props }: AccordionProps): ReactElement => {
|
||||
return (
|
||||
<StyledAccordion square elevation={0} $compact={compact} {...props}>
|
||||
{children}
|
||||
</StyledAccordion>
|
||||
)
|
||||
}
|
||||
|
||||
export const AccordionSummary = ({ children, ...props }: AccordionSummaryMUIProps): ReactElement => {
|
||||
return (
|
||||
<StyledAccordionSummary expandIcon={<FixedIcon type="chevronDown" />} {...props}>
|
||||
{children}
|
||||
</StyledAccordionSummary>
|
||||
)
|
||||
}
|
||||
|
||||
export { default as AccordionActions } from '@mui/material/AccordionActions'
|
||||
export { default as AccordionDetails } from '@mui/material/AccordionDetails'
|
||||
@@ -0,0 +1,204 @@
|
||||
import React, { ReactElement, ReactNode, HTMLAttributes } from 'react'
|
||||
import ButtonMUI, { ButtonProps as ButtonMUIProps } from '@mui/material/Button'
|
||||
import { alpha } from '@mui/material/styles'
|
||||
|
||||
import styled, { css, DefaultTheme, FlattenInterpolation, ThemeProps } from 'styled-components'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
type Colors = 'primary' | 'secondary' | 'error'
|
||||
type Variations = 'bordered' | 'contained' | 'outlined'
|
||||
|
||||
type CustomButtonMuiProps = Omit<ButtonMUIProps, 'size' | 'color' | 'variant'> & {
|
||||
to?: string
|
||||
component?: ReactNode
|
||||
}
|
||||
type LocalProps = {
|
||||
children?: ReactNode
|
||||
color?: Colors
|
||||
variant?: Variations
|
||||
iconType?: IconProps['type']
|
||||
iconSize?: IconProps['size']
|
||||
}
|
||||
|
||||
type Props = LocalProps & CustomButtonMuiProps & HTMLAttributes<HTMLButtonElement>
|
||||
|
||||
const StyledIcon = styled(Icon)<IconProps>`
|
||||
margin-right: 5px;
|
||||
`
|
||||
|
||||
const customStyles: {
|
||||
[key in Colors]: {
|
||||
[key in Variations]: FlattenInterpolation<ThemeProps<DefaultTheme>>
|
||||
}
|
||||
} = {
|
||||
primary: {
|
||||
contained: css`
|
||||
color: ${({ theme }) => theme.palette.background.main};
|
||||
background-color: ${({ theme }) => theme.palette.primary.main};
|
||||
box-shadow: 1px 2px 10px ${alpha('#28363D', 0.18)};
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.palette.background.main};
|
||||
background-color: ${({ theme }) => theme.palette.primary.dark};
|
||||
}
|
||||
`,
|
||||
outlined: css`
|
||||
color: ${({ theme }) => theme.palette.primary.main};
|
||||
background-color: transparent;
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.primary.main};
|
||||
}
|
||||
|
||||
&.Mui-disabled {
|
||||
color: ${({ theme }) => theme.palette.primary.main};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.palette.primary.dark};
|
||||
}
|
||||
`,
|
||||
bordered: css`
|
||||
color: ${({ theme }) => theme.palette.primary.main};
|
||||
background-color: transparent;
|
||||
border: 2px solid ${({ theme }) => theme.palette.primary.main};
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.primary.main};
|
||||
}
|
||||
|
||||
&.Mui-disabled {
|
||||
color: ${({ theme }) => theme.palette.primary.main};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.palette.background.light};
|
||||
}
|
||||
`,
|
||||
},
|
||||
secondary: {
|
||||
contained: css`
|
||||
color: ${({ theme }) => theme.palette.primary.main};
|
||||
background-color: ${({ theme }) => theme.palette.secondary.main};
|
||||
box-shadow: 1px 2px 10px ${alpha('#28363D', 0.18)};
|
||||
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.primary.main};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.primary.main};
|
||||
}
|
||||
|
||||
background-color: ${({ theme }) => theme.palette.secondary.dark};
|
||||
}
|
||||
`,
|
||||
outlined: css`
|
||||
color: ${({ theme }) => theme.palette.secondary.main};
|
||||
background-color: transparent;
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.secondary.main};
|
||||
}
|
||||
|
||||
&.Mui-disabled {
|
||||
color: ${({ theme }) => theme.palette.secondary.main};
|
||||
}
|
||||
`,
|
||||
bordered: css`
|
||||
color: ${({ theme }) => theme.palette.secondary.main};
|
||||
background-color: transparent;
|
||||
border: 2px solid ${({ theme }) => theme.palette.secondary.main};
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.secondary.main};
|
||||
}
|
||||
|
||||
&.Mui-disabled {
|
||||
color: ${({ theme }) => theme.palette.secondary.main};
|
||||
}
|
||||
`,
|
||||
},
|
||||
error: {
|
||||
contained: css`
|
||||
color: ${({ theme }) => theme.palette.error.main};
|
||||
background-color: ${({ theme }) => theme.palette.error.background};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.palette.error.light};
|
||||
color: ${({ theme }) => theme.palette.error.dark};
|
||||
}
|
||||
`,
|
||||
outlined: css`
|
||||
color: ${({ theme }) => theme.palette.error.main};
|
||||
background-color: transparent;
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.error.main};
|
||||
}
|
||||
|
||||
&.Mui-disabled {
|
||||
color: ${({ theme }) => theme.palette.error.main};
|
||||
}
|
||||
`,
|
||||
bordered: css`
|
||||
color: ${({ theme }) => theme.palette.error.main};
|
||||
background-color: transparent;
|
||||
border: 2px solid ${({ theme }) => theme.palette.error.main};
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.error.main};
|
||||
}
|
||||
|
||||
&.Mui-disabled {
|
||||
color: ${({ theme }) => theme.palette.error.main};
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
const StyledButton = styled(ButtonMUI)<{ $localProps: LocalProps }>`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
padding: 8px 1.4rem;
|
||||
min-width: 120px;
|
||||
|
||||
&.MuiButton-root {
|
||||
text-transform: none;
|
||||
border-radius: 8px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
&.Mui-disabled {
|
||||
color: ${({ theme }) => theme.palette.background.main};
|
||||
}
|
||||
|
||||
path.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.background.main};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
${({ $localProps }) => {
|
||||
if ($localProps.color !== undefined && $localProps.variant !== undefined) {
|
||||
return customStyles[$localProps.color][$localProps.variant]
|
||||
}
|
||||
}}
|
||||
}
|
||||
`
|
||||
|
||||
const Button = ({
|
||||
children,
|
||||
color = 'primary',
|
||||
variant = 'contained',
|
||||
iconType,
|
||||
iconSize,
|
||||
// We need destructuring all LocalProps, remaining props are for CustomButtonMuiProps
|
||||
...buttonMuiProps
|
||||
}: Props): ReactElement => {
|
||||
return (
|
||||
<StyledButton className={`${color} ${variant}`} {...buttonMuiProps} $localProps={{ color, variant }}>
|
||||
{iconType && iconSize && <StyledIcon size={iconSize} type={iconType} />}
|
||||
{children}
|
||||
</StyledButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { alpha } from '@mui/material/styles'
|
||||
|
||||
const StyledCard = styled.div`
|
||||
box-shadow: 1px 2px 10px 0 ${alpha('#28363D', 0.18)};
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
background-color: ${({ theme }) => theme.palette.common.white};
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const Disabled = styled.div`
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: ${({ theme }) => theme.palette.common.white};
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const Card: React.FC<Props> = ({ className, children, disabled, ...rest }): React.ReactElement => (
|
||||
<StyledCard className={className} {...rest}>
|
||||
{disabled && <Disabled />}
|
||||
{children}
|
||||
</StyledCard>
|
||||
)
|
||||
|
||||
export default Card
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import MuiAlert from '@mui/lab/Alert'
|
||||
import MuiAlertTitle from '@mui/lab/AlertTitle'
|
||||
import styled from 'styled-components'
|
||||
import { useTransactionLibrary } from '../store'
|
||||
|
||||
const ChecksumWarning = () => {
|
||||
const { hasChecksumWarning, setHasChecksumWarning } = useTransactionLibrary()
|
||||
|
||||
if (!hasChecksumWarning) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ChecksumWrapper>
|
||||
<MuiAlert severity="warning" onClose={() => setHasChecksumWarning(false)}>
|
||||
<MuiAlertTitle>This batch contains some changed properties since you saved or downloaded it</MuiAlertTitle>
|
||||
</MuiAlert>
|
||||
</ChecksumWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const ChecksumWrapper = styled.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
background-color: transparent;
|
||||
height: 70px;
|
||||
`
|
||||
|
||||
export default ChecksumWarning
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useContext, useRef } from 'react'
|
||||
import { alpha } from '@mui/material'
|
||||
import Hidden from '@mui/material/Hidden'
|
||||
import styled from 'styled-components'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
import { ReactComponent as CreateNewBatchLightSvg } from '../assets/new-batch-light.svg'
|
||||
import { ReactComponent as CreateNewBatchDarkSvg } from '../assets/new-batch-dark.svg'
|
||||
import { ReactComponent as ArrowBlock } from '../assets/arrowtotheblock.svg'
|
||||
import useDropZone from '../hooks/useDropZone'
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import { Icon } from './Icon'
|
||||
import Text from './Text'
|
||||
import ButtonLink from './buttons/ButtonLink'
|
||||
import { EModes, ThemeModeContext } from '../theme/SafeThemeProvider'
|
||||
|
||||
type CreateNewBatchCardProps = {
|
||||
onFileSelected: (file: File | null) => void
|
||||
}
|
||||
|
||||
const CreateNewBatchCard = ({ onFileSelected }: CreateNewBatchCardProps) => {
|
||||
const theme = useTheme()
|
||||
const mode = useContext(ThemeModeContext)
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
|
||||
const fileRef = useRef<HTMLInputElement | null>(null)
|
||||
const { isOverDropZone, isAcceptError, dropHandlers } = useDropZone((file: File | null) => {
|
||||
onFileSelected(file)
|
||||
}, '.json')
|
||||
|
||||
const handleFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.preventDefault()
|
||||
if (event.target.files?.length) {
|
||||
onFileSelected(event.target.files[0])
|
||||
event.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleBrowse = function (event: React.MouseEvent) {
|
||||
event.preventDefault()
|
||||
fileRef.current?.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper isSmallScreen={isSmallScreen}>
|
||||
<Hidden smDown>
|
||||
{mode === EModes.DARK ? <CreateNewBatchDarkSvg /> : <CreateNewBatchLightSvg />}
|
||||
<StyledArrowBlock />
|
||||
</Hidden>
|
||||
|
||||
<StyledCreateBatchContent>
|
||||
<StyledText variant="body1">Start creating a new batch </StyledText>
|
||||
<StyledText variant="body1">or</StyledText>
|
||||
<StyledDragAndDropFileContainer
|
||||
{...dropHandlers}
|
||||
dragOver={isOverDropZone}
|
||||
fullWidth={isSmallScreen}
|
||||
error={isAcceptError}
|
||||
>
|
||||
{isAcceptError ? (
|
||||
<StyledText variant="body1" error={isAcceptError}>
|
||||
The uploaded file is not a valid JSON file
|
||||
</StyledText>
|
||||
) : (
|
||||
<>
|
||||
<Icon type="termsOfUse" size="sm" />
|
||||
<StyledText variant="body1">Drag and drop a JSON file or</StyledText>
|
||||
<StyledButtonLink color="secondary" onClick={handleBrowse}>
|
||||
choose a file
|
||||
</StyledButtonLink>
|
||||
</>
|
||||
)}
|
||||
</StyledDragAndDropFileContainer>
|
||||
<input ref={fileRef} id="logo-input" type="file" onChange={handleFileSelected} accept=".json" hidden />
|
||||
</StyledCreateBatchContent>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateNewBatchCard
|
||||
|
||||
const Wrapper = styled.div<{ isSmallScreen: boolean }>`
|
||||
text-align: center;
|
||||
position: relative;
|
||||
margin-top: ${({ isSmallScreen }) => (isSmallScreen ? '0' : '64px')};
|
||||
`
|
||||
|
||||
const StyledArrowBlock = styled(ArrowBlock)`
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
top: 7rem;
|
||||
`
|
||||
|
||||
const StyledCreateBatchContent = styled.div`
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
`
|
||||
|
||||
const StyledDragAndDropFileContainer = styled.div<{
|
||||
dragOver: boolean
|
||||
fullWidth: boolean
|
||||
error: boolean
|
||||
}>`
|
||||
box-sizing: border-box;
|
||||
max-width: ${({ fullWidth }) => (fullWidth ? '100%' : '430px')};
|
||||
width: 100%;
|
||||
border: 2px dashed ${({ theme, error }) => (error ? theme.palette.error.main : theme.palette.secondary.dark)};
|
||||
border-radius: 8px;
|
||||
background-color: ${({ theme, error }) =>
|
||||
error ? alpha(theme.palette.error.main, 0.7) : theme.palette.secondary.background};
|
||||
padding: 24px;
|
||||
margin: 6px auto;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
${({ dragOver, error, theme }) => {
|
||||
if (dragOver) {
|
||||
return `
|
||||
transition: all 0.2s ease-in-out;
|
||||
transform: scale(1.05);
|
||||
`
|
||||
}
|
||||
|
||||
return `
|
||||
border-color: ${error ? theme.palette.error.main : theme.palette.secondary.dark};
|
||||
background-color: ${error ? alpha(theme.palette.error.main, 0.7) : theme.palette.secondary.background};
|
||||
`
|
||||
}}
|
||||
`
|
||||
|
||||
const StyledText = styled(Text)<{ error?: boolean }>`
|
||||
&& {
|
||||
color: ${({ error, theme }) => (error ? theme.palette.common.white : theme.palette.text.secondary)};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledButtonLink = styled(ButtonLink)`
|
||||
margin-left: 0.3rem;
|
||||
padding: 0;
|
||||
|
||||
&& > p {
|
||||
color: ${({ theme }) => theme.palette.upload.primary};
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.palette.backdrop.main};
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
orientation?: 'vertical' | 'horizontal'
|
||||
}
|
||||
|
||||
const HorizontalDivider = styled.div`
|
||||
margin: 16px -1.6rem;
|
||||
border-top: solid 1px ${({ theme }) => theme.palette.border.light};
|
||||
width: calc(100% + 3.2rem);
|
||||
`
|
||||
|
||||
const VerticalDivider = styled.div`
|
||||
border-right: 1px solid ${({ theme }) => theme.legacy.colors.separator};
|
||||
margin: 0 5px;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const Divider = ({ className, orientation }: Props): React.ReactElement => {
|
||||
return orientation === 'vertical' ? (
|
||||
<VerticalDivider className={className} />
|
||||
) : (
|
||||
<HorizontalDivider className={className} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Divider
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { type Theme } from '@mui/material/styles'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
color: keyof Theme['palette']
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const StyledDot = styled.div<Props>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
background-color: ${({ theme, color }) => theme.palette[color].main};
|
||||
`
|
||||
|
||||
const Dot: React.FC<Props> = ({ children, ...rest }): React.ReactElement => <StyledDot {...rest}>{children}</StyledDot>
|
||||
|
||||
export default Dot
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { textShortener } from '../utils/strings'
|
||||
import Text from './Text'
|
||||
import { Theme } from '@mui/material'
|
||||
import ExplorerButton from './buttons/ExplorerButton'
|
||||
import Identicon, { identiconSizes } from './buttons/Identicon'
|
||||
import CopyToClipboardBtn from './buttons/CopyToClipboardBtn'
|
||||
import EllipsisMenu from './EllipsisMenu'
|
||||
|
||||
export type ExplorerInfo = () => { url: string; alt: string }
|
||||
|
||||
type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
export interface EllipsisMenuItem {
|
||||
label: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const AvatarContainer = styled.div`
|
||||
display: flex;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const InfoContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const AddressContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const StyledImg = styled.img<{ size: SizeType }>`
|
||||
height: ${({ size }) => identiconSizes[size]};
|
||||
width: ${({ size }) => identiconSizes[size]};
|
||||
`
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
hash: string
|
||||
showHash?: boolean
|
||||
shortenHash?: number
|
||||
name?: string
|
||||
strongName?: boolean
|
||||
textColor?: keyof Theme['palette']
|
||||
textSize?: SizeType
|
||||
showAvatar?: boolean
|
||||
customAvatar?: string
|
||||
customAvatarFallback?: string
|
||||
avatarSize?: SizeType
|
||||
showCopyBtn?: boolean
|
||||
menuItems?: EllipsisMenuItem[]
|
||||
explorerUrl?: ExplorerInfo
|
||||
}
|
||||
|
||||
type ShortNameProps =
|
||||
| {
|
||||
shouldShowShortName: boolean
|
||||
shouldCopyShortName?: boolean
|
||||
shortName: string
|
||||
}
|
||||
| {
|
||||
shouldShowShortName?: boolean
|
||||
shouldCopyShortName: boolean
|
||||
shortName: string
|
||||
}
|
||||
| {
|
||||
shouldShowShortName?: never
|
||||
shouldCopyShortName?: never
|
||||
shortName?: string
|
||||
}
|
||||
|
||||
type EthHashInfoProps = Props & ShortNameProps
|
||||
|
||||
const EthHashInfo = ({
|
||||
hash,
|
||||
showHash = true,
|
||||
name,
|
||||
className,
|
||||
shortenHash,
|
||||
showAvatar,
|
||||
customAvatar,
|
||||
customAvatarFallback,
|
||||
avatarSize = 'md',
|
||||
showCopyBtn,
|
||||
menuItems,
|
||||
explorerUrl,
|
||||
shortName,
|
||||
shouldShowShortName,
|
||||
shouldCopyShortName,
|
||||
}: EthHashInfoProps): React.ReactElement => {
|
||||
const [fallbackToIdenticon, setFallbackToIdenticon] = useState(false)
|
||||
const [fallbackSrc, setFallabckSrc] = useState<undefined | string>(undefined)
|
||||
|
||||
const setAppImageFallback = (): void => {
|
||||
if (customAvatarFallback && !fallbackToIdenticon) {
|
||||
setFallabckSrc(customAvatarFallback)
|
||||
} else {
|
||||
setFallbackToIdenticon(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer className={className}>
|
||||
{showAvatar && (
|
||||
<AvatarContainer>
|
||||
{!fallbackToIdenticon && customAvatar ? (
|
||||
<StyledImg src={fallbackSrc || customAvatar} size={avatarSize} onError={setAppImageFallback} />
|
||||
) : (
|
||||
<Identicon address={hash} size={avatarSize} />
|
||||
)}
|
||||
</AvatarContainer>
|
||||
)}
|
||||
|
||||
<InfoContainer>
|
||||
{name && <Text>{name}</Text>}
|
||||
<AddressContainer>
|
||||
{showHash && (
|
||||
<Text>
|
||||
{shouldShowShortName && (
|
||||
<Text component="span" strong>
|
||||
{shortName}:
|
||||
</Text>
|
||||
)}
|
||||
{shortenHash ? textShortener(hash, shortenHash + 2, shortenHash) : hash}
|
||||
</Text>
|
||||
)}
|
||||
{showCopyBtn && <CopyToClipboardBtn textToCopy={shouldCopyShortName ? `${shortName}:${hash}` : hash} />}
|
||||
{explorerUrl && <ExplorerButton explorerUrl={explorerUrl} />}
|
||||
{menuItems && <EllipsisMenu menuItems={menuItems} />}
|
||||
</AddressContainer>
|
||||
</InfoContainer>
|
||||
</StyledContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default EthHashInfo
|
||||
@@ -0,0 +1,47 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export type EditableLabelProps = {
|
||||
children: React.ReactNode
|
||||
onEdit: (value: string) => void
|
||||
}
|
||||
|
||||
const EditableLabel = ({ children, onEdit }: EditableLabelProps) => {
|
||||
return (
|
||||
<EditableComponent
|
||||
contentEditable="true"
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(event) => onEdit(event.target.innerText)}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
;(event.target as HTMLDivElement).blur()
|
||||
event.preventDefault()
|
||||
}
|
||||
}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</EditableComponent>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditableLabel
|
||||
|
||||
const EditableComponent = styled.div`
|
||||
font-family: Averta, 'Roboto', sans-serif;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
padding: 10px;
|
||||
cursor: text;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: #e2e3e3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline-color: #008c73;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ClickAwayListener } from '@mui/material'
|
||||
import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import FixedIcon from '../FixedIcon'
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
&& {
|
||||
.MuiMenu-paper {
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.MuiMenu-list {
|
||||
div:not(:first-child) {
|
||||
border-top: 1px solid ${({ theme }) => theme.palette.divider};
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MenuWrapper = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const MenuItemWrapper = styled.div`
|
||||
:focus {
|
||||
outline-color: ${({ theme }) => theme.palette.divider};
|
||||
}
|
||||
`
|
||||
|
||||
const IconWrapper = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
outline-color: transparent;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.palette.divider};
|
||||
}
|
||||
`
|
||||
|
||||
export type EllipsisMenuItem = {
|
||||
label: string
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
type Props = {
|
||||
menuItems: EllipsisMenuItem[]
|
||||
}
|
||||
|
||||
const EllipsisMenu = ({ menuItems }: Props): React.ReactElement => {
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null)
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>): void => setAnchorEl(event.currentTarget)
|
||||
|
||||
const closeMenuHandler = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const onMenuItemClick = (item: EllipsisMenuItem) => {
|
||||
item.onClick()
|
||||
closeMenuHandler()
|
||||
}
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={closeMenuHandler}>
|
||||
<MenuWrapper>
|
||||
<IconWrapper onClick={handleClick}>
|
||||
<FixedIcon type="options" />
|
||||
</IconWrapper>
|
||||
<StyledMenu anchorEl={anchorEl} keepMounted onClose={closeMenuHandler} open={Boolean(anchorEl)}>
|
||||
{menuItems.map((item) => (
|
||||
<MenuItemWrapper key={item.label}>
|
||||
<MenuItem disabled={item.disabled} onClick={() => onMenuItemClick(item)}>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
</MenuItemWrapper>
|
||||
))}
|
||||
</StyledMenu>
|
||||
</MenuWrapper>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
}
|
||||
|
||||
export default EllipsisMenu
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import MuiAlert from '@mui/lab/Alert'
|
||||
import MuiAlertTitle from '@mui/lab/AlertTitle'
|
||||
import styled from 'styled-components'
|
||||
import { useTransactionLibrary } from '../store'
|
||||
|
||||
const ErrorAlert = () => {
|
||||
const { errorMessage, setErrorMessage } = useTransactionLibrary()
|
||||
|
||||
if (!errorMessage) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorAlertContainer>
|
||||
<MuiAlert severity="error" onClose={() => setErrorMessage('')}>
|
||||
<MuiAlertTitle>{errorMessage}</MuiAlertTitle>
|
||||
</MuiAlert>
|
||||
</ErrorAlertContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorAlertContainer = styled.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
background-color: transparent;
|
||||
height: 70px;
|
||||
`
|
||||
|
||||
export default ErrorAlert
|
||||
@@ -0,0 +1,12 @@
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||
<path
|
||||
fill="#008C73"
|
||||
fillRule="evenodd"
|
||||
d="M3.431 7.99h2.6c.554 0 1.004.45 1.004 1.005C7.035 9.55 6.585 10 6.03 10H1.005c-.277 0-.529-.112-.71-.294C.111 9.524 0 9.272 0 8.995V3.97c0-.555.45-1.005 1.005-1.005.555 0 1.005.45 1.005 1.005v2.599L8.284.294c.393-.392 1.03-.392 1.422 0 .392.393.392 1.03 0 1.422L3.43 7.99z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||
<path
|
||||
fill="#FFFFFF"
|
||||
fillRule="evenodd"
|
||||
d="M3.431 7.99h2.6c.554 0 1.004.45 1.004 1.005C7.035 9.55 6.585 10 6.03 10H1.005c-.277 0-.529-.112-.71-.294C.111 9.524 0 9.272 0 8.995V3.97c0-.555.45-1.005 1.005-1.005.555 0 1.005.45 1.005 1.005v2.599L8.284.294c.393-.392 1.03-.392 1.422 0 .392.393.392 1.03 0 1.422L3.43 7.99z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||
<path
|
||||
fill="#F02525"
|
||||
fillRule="evenodd"
|
||||
d="M6.569 2.01h-2.6c-.554 0-1.004-.45-1.004-1.005C2.965.45 3.415 0 3.97 0h5.025c.277 0 .529.112.71.294.183.182.295.434.295.711V6.03c0 .555-.45 1.005-1.005 1.005-.555 0-1.005-.45-1.005-1.005V3.431L1.716 9.706c-.393.392-1.03.392-1.422 0-.392-.393-.392-1.03 0-1.422L6.57 2.01z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
|
||||
<path
|
||||
fill="#FFFFFF"
|
||||
fillRule="evenodd"
|
||||
d="M6.569 2.01h-2.6c-.554 0-1.004-.45-1.004-1.005C2.965.45 3.415 0 3.97 0h5.025c.277 0 .529.112.71.294.183.182.295.434.295.711V6.03c0 .555-.45 1.005-1.005 1.005-.555 0-1.005-.45-1.005-1.005V3.431L1.716 9.706c-.393.392-1.03.392-1.422 0-.392-.393-.392-1.03 0-1.422L6.57 2.01z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
|
||||
<path
|
||||
fill="#5D6D74"
|
||||
fillRule="evenodd"
|
||||
d="M4.984 4.608l1.302-1.322c.384-.384 1.024-.384 1.408 0 .384.384.384 1.024 0 1.408L4.728 7.702C4.537 7.893 4.28 8 4.024 8c-.256 0-.511-.107-.704-.298L.29 4.694c-.426-.427-.383-1.152.13-1.515.404-.298.98-.213 1.322.15l1.13 1.108.129.022V.982C3 .427 3.447 0 3.98 0c.577 0 1.004.448 1.004.982v3.626z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="6" viewBox="0 0 6 6">
|
||||
<path
|
||||
fill="#008C73"
|
||||
fillRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M3 0C1.347 0 0 1.347 0 3s1.347 3 3 3 3-1.347 3-3-1.347-3-3-3z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7">
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
fillRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M6.709 6.709c-.195.195-.452.292-.71.29-.257.002-.514-.095-.71-.29l-.046-.05L.292 1.706c-.39-.39-.39-1.025 0-1.414.389-.39 1.025-.39 1.414 0l4.293 4.294L10.293.292c.389-.39 1.025-.39 1.414 0 .389.389.389 1.025 0 1.414L6.763 6.649l-.054.06z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12">
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
className="icon-color"
|
||||
fillRule="evenodd"
|
||||
d="M.291 5.29l.06-.054L5.294.292c.389-.39 1.025-.39 1.414 0 .389.389.389 1.025 0 1.414L2.414 6l4.294 4.293c.389.389.389 1.025 0 1.414s-1.025.389-1.414 0L.341 6.756l-.05-.047c-.195-.195-.292-.452-.291-.71-.001-.257.096-.514.291-.71z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12">
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
className="icon-color"
|
||||
fillRule="evenodd"
|
||||
d="M6.709 5.29l-.06-.054L1.706.292c-.39-.39-1.025-.39-1.414 0-.39.389-.39 1.025 0 1.414L4.586 6 .292 10.293c-.39.389-.39 1.025 0 1.414.389.389 1.025.389 1.414 0l4.953-4.951.05-.047c.195-.195.292-.452.29-.71.002-.257-.095-.514-.29-.71"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7">
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
fillRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M6.709.291l.054.06 4.944 4.943c.389.389.389 1.025 0 1.414-.39.389-1.025.389-1.414 0L5.999 2.414 1.706 6.708c-.39.389-1.025.389-1.414 0-.39-.389-.39-1.025 0-1.414L5.242.341l.048-.05c.195-.195.452-.292.709-.291.258-.001.515.096.71.291z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9">
|
||||
<path
|
||||
fill="#E8673C"
|
||||
d="M4.5 9C6.985 9 9 6.985 9 4.5S6.985 0 4.5 0 0 2.015 0 4.5 2.015 9 4.5 9zm0-2C3.12 7 2 5.88 2 4.5S3.12 2 4.5 2 7 3.12 7 4.5 5.88 7 4.5 7z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9">
|
||||
<path
|
||||
fill="#008C73"
|
||||
d="M4.5 9C6.985 9 9 6.985 9 4.5S6.985 0 4.5 0 0 2.015 0 4.5 2.015 9 4.5 9zm0-2C3.12 7 2 5.88 2 4.5S3.12 2 4.5 2 7 3.12 7 4.5 5.88 7 4.5 7z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 34 34">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<circle cx="17" cy="17" r="17" fill="#008C73" />
|
||||
<path
|
||||
fill="#FFF"
|
||||
fillRule="nonzero"
|
||||
className="icon-color"
|
||||
d="M17 27c5.523 0 10-4.477 10-10S22.523 7 17 7c-5.462 0-9.911 4.382-9.999 9.836-.009.553.432 1.007.984 1.016.552.01 1.007-.431 1.016-.984C9.071 12.506 12.631 9 17 9c4.418 0 8 3.582 8 8s-3.582 8-8 8c-.552 0-1 .448-1 1s.448 1 1 1z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="5" viewBox="0 0 9 5">
|
||||
<path
|
||||
fill="#5D6D74"
|
||||
d="M3.858 4.754c.355.328.93.328 1.284 0l3.59-3.32C9.305.906 8.899 0 8.09 0H.91C.101 0-.305.907.268 1.436l3.59 3.319z"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" viewBox="0 0 90 90">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<circle cx="45" cy="45" r="45" fill="#E8E7E6" />
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
fillRule="nonzero"
|
||||
d="M26.24 62c-.57 0-1.13-.15-1.623-.437a3.254 3.254 0 0 1-1.18-4.44l18.76-32.501a3.234 3.234 0 0 1 5.606 0l18.761 32.501c.286.495.436 1.057.436 1.628A3.244 3.244 0 0 1 63.76 62H26.24z"
|
||||
/>
|
||||
<path fill="#E8E7E6" fillRule="nonzero" d="M47 49h-4V36h4zM47 55h-4v-4h4z" />
|
||||
<circle cx="78" cy="78" r="11" fill="#FFC05F" fillRule="nonzero" stroke="#F6F7F8" strokeWidth="4" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<circle id="ca7p6cg2da" cx="20" cy="20" r="20" />
|
||||
</defs>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<circle cx="20" cy="20" r="20" transform="matrix(1, 0, 0, 1, 0, 0)" fill="#E8E7E6" />
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
d="M 18.02 29.05 L 22.894 29.05 C 23.384 29.05 23.783 28.652 23.783 28.161 C 23.783 28.113 23.779 28.066 23.771 28.018 L 22.519 20.313 C 24.103 19.553 25.203 17.958 25.203 16.104 C 25.203 13.524 23.079 11.429 20.458 11.429 C 17.84 11.429 15.714 13.524 15.714 16.104 C 15.711 17.96 16.81 19.554 18.397 20.314 L 17.143 28.018 C 17.064 28.503 17.393 28.959 17.878 29.038 C 17.925 29.046 17.973 29.05 18.021 29.05 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="108" height="96" viewBox="0 0 108 96">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H108V96H0z" opacity=".557" />
|
||||
<g>
|
||||
<path
|
||||
fill="#F6F7F8"
|
||||
d="M49 0c25.405 0 46 20.595 46 46S74.405 92 49 92 3 71.405 3 46 23.595 0 49 0"
|
||||
transform="translate(5 4)"
|
||||
/>
|
||||
<path
|
||||
fill="#F02525"
|
||||
d="M42.12 36.726c0-3.675 2.99-6.665 6.665-6.665s6.665 2.99 6.665 6.665-2.99 6.664-6.665 6.664-6.664-2.989-6.664-6.664m26.26 20.668c-3.41-6.652-8.073-10.436-14.173-11.5 3.132-1.86 5.242-5.269 5.242-9.168 0-5.88-4.784-10.665-10.665-10.665-5.88 0-10.664 4.784-10.664 10.665 0 4.317 2.584 8.035 6.282 9.711-9.101 2.803-12.981 11.934-15.353 20.066-.31 1.06.3 2.17 1.36 2.48.187.055.375.081.56.081.867 0 1.665-.567 1.92-1.442 3.69-12.652 8.945-18.05 17.57-18.053 6.424.001 10.987 3.067 14.362 9.65.502.983 1.706 1.372 2.692.866.983-.504 1.37-1.709.867-2.69"
|
||||
transform="translate(5 4)"
|
||||
/>
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
d="M21.496 37.329c-3.675 0-6.665-2.99-6.665-6.665S17.82 24 21.496 24s6.664 2.989 6.664 6.664-2.99 6.665-6.664 6.665m14.07 15.289c.872-1.088 1.829-2.099 2.908-2.984-3.065-5.177-7.032-8.329-12.003-9.544 3.379-1.791 5.689-5.342 5.689-9.426C32.16 24.784 27.376 20 21.496 20c-5.881 0-10.665 4.784-10.665 10.664 0 4.077 2.302 7.623 5.672 9.418C6.632 42.477 2.543 51.998.08 60.442c-.31 1.06.3 2.17 1.36 2.48.187.054.375.081.56.081.867 0 1.665-.568 1.92-1.442 3.69-12.652 8.945-18.052 17.57-18.054 6.228.001 10.729 2.906 14.075 9.111M76.083 37.329c-3.675 0-6.664-2.99-6.664-6.665S72.408 24 76.083 24s6.664 2.989 6.664 6.664-2.99 6.665-6.664 6.665M97.49 60.442c-2.462-8.443-6.55-17.962-16.417-20.359 3.37-1.794 5.674-5.341 5.674-9.419 0-5.88-4.784-10.664-10.664-10.664s-10.664 4.784-10.664 10.664c0 4.078 2.303 7.625 5.674 9.419-4.448 1.082-8.075 3.714-10.981 7.955 1.135.665 2.203 1.464 3.205 2.404 3.18-4.716 7.27-6.934 12.763-6.935 8.625.002 13.88 5.402 17.57 18.054.255.874 1.053 1.442 1.919 1.442.186 0 .374-.027.56-.081 1.06-.31 1.67-1.42 1.36-2.48"
|
||||
transform="translate(5 4)"
|
||||
/>
|
||||
<path
|
||||
fill="#F02525"
|
||||
fillRule="nonzero"
|
||||
d="M76.5 61C83.956 61 90 67.044 90 74.5S83.956 88 76.5 88 63 81.956 63 74.5 69.044 61 76.5 61zm8.026 8.415l-13.111 13.11C72.885 83.46 74.629 84 76.5 84c5.247 0 9.5-4.253 9.5-9.5 0-1.87-.54-3.615-1.474-5.085zM76.5 65c-5.247 0-9.5 4.253-9.5 9.5 0 1.929.575 3.723 1.562 5.221l13.159-13.159C80.223 65.575 78.429 65 76.5 65z"
|
||||
transform="translate(5 4)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="4" viewBox="0 0 16 4">
|
||||
<g fill="#B2B5B2" className="icon-color" fillRule="evenodd">
|
||||
<circle cx="2" cy="2" r="2" />
|
||||
<circle cx="8" cy="2" r="2" />
|
||||
<circle cx="14" cy="2" r="2" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,20 @@
|
||||
const icon = (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.7895 4C12.3708 4 12.8421 4.47128 12.8421 5.05263L12.8421 18.9474C12.8421 19.5287 12.3708 20 11.7895 20C11.2081 20 10.7368 19.5287 10.7368 18.9474L10.7368 5.05263C10.7368 4.47128 11.2081 4 11.7895 4Z"
|
||||
fill="#F4F4F4"
|
||||
className="icon-color"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M20 11.7895C20 12.3708 19.5287 12.8421 18.9474 12.8421L5.05263 12.8421C4.47128 12.8421 4 12.3708 4 11.7895C4 11.2081 4.47128 10.7368 5.05263 10.7368L18.9474 10.7368C19.5287 10.7368 20 11.2081 20 11.7895Z"
|
||||
fill="#F4F4F4"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
|
||||
<path
|
||||
fill="#B2B5B2"
|
||||
fillRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M7 10c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3m6.068-4.209l-.3-.03c-.683-.068-1-.437-1.137-.67-.068-.255-.11-.74.316-1.286l.199-.242c.54-.607.354-.963-.035-1.352l-.322-.32c-.39-.39-.744-.576-1.353-.037l-.235.192c-.535.44-1.025.397-1.285.328-.232-.135-.609-.45-.677-1.14l-.03-.3C8.16.12 7.777 0 7.228 0h-.456c-.55 0-.932.121-.98.933l-.03.31c-.084.686-.458 1-.686 1.133-.261.068-.747.105-1.277-.33l-.235-.192c-.609-.539-.964-.353-1.353.036l-.322.321c-.389.39-.575.745-.035 1.352l.2.242c.424.545.383 1.031.315 1.286-.137.233-.454.602-1.137.67l-.3.03c-.811.05-.932.431-.932.981v.456c0 .55.121.933.932.98l.31.03c.686.085 1 .458 1.133.686.068.262.105.748-.33 1.278l-.192.235c-.539.607-.353.963.036 1.352l.322.32c.39.39.744.576 1.353.037l.242-.2c.539-.421 1.02-.384 1.277-.317.229.135.596.449.679 1.128l.03.31c.048.812.43.933.98.933h.456c.55 0 .932-.121.981-.932l.03-.301c.067-.683.437-1 .67-1.137.255-.067.741-.11 1.285.316l.242.2c.609.539.964.354 1.353-.036l.322-.321c.389-.39.575-.745.036-1.352l-.192-.235c-.435-.53-.398-1.016-.33-1.278.133-.228.447-.601 1.133-.686l.31-.03c.811-.047.932-.43.932-.98v-.456c0-.55-.121-.932-.932-.98"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
const icon = (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="4" viewBox="0 0 20 4">
|
||||
<g fill="#B2B5B2" className="icon-color" fillRule="evenodd">
|
||||
<rect width="4" height="4" x="16" rx="2" />
|
||||
<rect width="4" height="4" x="8" rx="2" />
|
||||
<rect width="4" height="4" rx="2" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default icon
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
|
||||
import arrowSort from './images/arrowSort'
|
||||
import connectedRinkeby from './images/connectedRinkeby'
|
||||
import connectedWallet from './images/connectedWallet'
|
||||
import bullit from './images/bullit'
|
||||
import dropdownArrowSmall from './images/dropdownArrowSmall'
|
||||
import arrowReceived from './images/arrowReceived'
|
||||
import arrowReceivedWhite from './images/arrowReceivedWhite'
|
||||
import arrowSent from './images/arrowSent'
|
||||
import arrowSentWhite from './images/arrowSentWhite'
|
||||
import threeDots from './images/threeDots'
|
||||
import options from './images/options'
|
||||
import plus from './images/plus'
|
||||
import chevronRight from './images/chevronRight'
|
||||
import chevronLeft from './images/chevronLeft'
|
||||
import chevronUp from './images/chevronUp'
|
||||
import chevronDown from './images/chevronDown'
|
||||
import settingsChange from './images/settingsChange'
|
||||
import creatingInProgress from './images/creatingInProgress'
|
||||
import notOwner from './images/notOwner'
|
||||
import notConnected from './images/notConnected'
|
||||
import networkError from './images/networkError'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledIcon = styled.span`
|
||||
.icon-color {
|
||||
fill: ${({ theme }) => theme.palette.text.primary};
|
||||
}
|
||||
|
||||
.icon-stroke {
|
||||
fill: ${({ theme }) => theme.palette.text.primary};
|
||||
}
|
||||
`
|
||||
const icons = {
|
||||
arrowSort,
|
||||
connectedRinkeby,
|
||||
connectedWallet,
|
||||
bullit,
|
||||
dropdownArrowSmall,
|
||||
arrowReceived,
|
||||
arrowReceivedWhite,
|
||||
arrowSent,
|
||||
arrowSentWhite,
|
||||
threeDots,
|
||||
options,
|
||||
plus,
|
||||
chevronRight,
|
||||
chevronLeft,
|
||||
chevronUp,
|
||||
chevronDown,
|
||||
settingsChange,
|
||||
creatingInProgress,
|
||||
notOwner,
|
||||
notConnected,
|
||||
networkError,
|
||||
}
|
||||
|
||||
export type IconType = typeof icons
|
||||
export type IconTypes = keyof IconType
|
||||
|
||||
type Props = {
|
||||
type: IconTypes
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The `FixedIcon` renders an icon
|
||||
*/
|
||||
function FixedIcon({ type, className }: Props): React.ReactElement {
|
||||
return <StyledIcon className={className}>{icons[type]}</StyledIcon>
|
||||
}
|
||||
|
||||
export default FixedIcon
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from 'react'
|
||||
import Modal from '@mui/material/Modal'
|
||||
import { alpha, styled as muiStyled } from '@mui/material/styles'
|
||||
import styled from 'styled-components'
|
||||
import Media from 'react-media'
|
||||
import { Typography } from '@mui/material'
|
||||
import { Icon } from './Icon'
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
|
||||
span {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: ${({ theme }) => theme.palette.divider};
|
||||
border-radius: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
const TitleSection = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 2px solid ${({ theme }) => theme.palette.divider};
|
||||
`
|
||||
|
||||
const BodySection = styled.div<{
|
||||
$withoutBodyPadding?: boolean
|
||||
$smallHeight: boolean
|
||||
}>`
|
||||
max-height: ${({ $smallHeight }) => ($smallHeight ? '280px' : '460px')};
|
||||
overflow-y: auto;
|
||||
padding: ${({ $withoutBodyPadding }) => ($withoutBodyPadding ? '0' : '16px 24px')};
|
||||
`
|
||||
|
||||
const FooterSection = styled.div`
|
||||
border-top: 2px solid ${({ theme }) => theme.palette.divider};
|
||||
padding: 16px 24px;
|
||||
`
|
||||
|
||||
const ModalPaper = styled.div`
|
||||
background: ${({ theme }) => theme.palette.background.paper};
|
||||
color: ${({ theme }) => theme.palette.text.primary};
|
||||
`
|
||||
|
||||
export type GenericModalProps = {
|
||||
title: string | React.ReactNode
|
||||
body: React.ReactNode
|
||||
withoutBodyPadding?: boolean
|
||||
footer?: React.ReactNode
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const StyledModal = muiStyled(Modal, {
|
||||
shouldForwardProp: (prop) => prop !== 'smallHeight',
|
||||
})<{ smallHeight: boolean }>(({ smallHeight }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflowY: 'scroll',
|
||||
background: alpha('#E8E7E6', 0.75),
|
||||
'& .MuiModal-paper': {
|
||||
position: smallHeight ? 'relative' : 'absolute',
|
||||
top: smallHeight ? 'unset' : '121px',
|
||||
minWidth: '500px',
|
||||
width: smallHeight ? '500px' : 'inherit',
|
||||
borderRadius: '8px',
|
||||
boxShadow: `0 0 0.75 0 #28363D`,
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const PaperWrapper = muiStyled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'smallHeight',
|
||||
})<{ smallHeight: boolean }>(({ smallHeight }) => ({
|
||||
position: smallHeight ? 'relative' : 'absolute',
|
||||
top: smallHeight ? 'unset' : '121px',
|
||||
minWidth: '500px',
|
||||
width: smallHeight ? '500px' : 'inherit',
|
||||
borderRadius: '8px',
|
||||
boxShadow: `0 0 0.75 0 #28363D`,
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
}))
|
||||
|
||||
const GenericModalComponent = ({
|
||||
body,
|
||||
footer,
|
||||
onClose,
|
||||
title,
|
||||
withoutBodyPadding,
|
||||
smallHeight,
|
||||
}: GenericModalProps & { smallHeight: boolean }) => {
|
||||
return (
|
||||
<StyledModal open smallHeight={smallHeight}>
|
||||
<PaperWrapper smallHeight={smallHeight}>
|
||||
<ModalPaper>
|
||||
<TitleSection>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<StyledButton onClick={onClose}>
|
||||
<Icon size="sm" type="cross" />
|
||||
</StyledButton>
|
||||
</TitleSection>
|
||||
|
||||
<BodySection $withoutBodyPadding={withoutBodyPadding} $smallHeight={smallHeight}>
|
||||
{body}
|
||||
</BodySection>
|
||||
|
||||
{footer && <FooterSection>{footer}</FooterSection>}
|
||||
</ModalPaper>
|
||||
</PaperWrapper>
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
||||
|
||||
const GenericModal = (props: GenericModalProps): React.ReactElement => (
|
||||
<Media query={{ maxHeight: 500 }}>
|
||||
{(matches: boolean) => <GenericModalComponent {...props} smallHeight={matches} />}
|
||||
</Media>
|
||||
)
|
||||
|
||||
export default GenericModal
|
||||
@@ -0,0 +1,27 @@
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
|
||||
import { render } from '../test-utils'
|
||||
import Header from './Header'
|
||||
|
||||
describe('<Header>', () => {
|
||||
it('Renders Header component', async () => {
|
||||
render(<Header />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Transaction Builder')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('Shows Link to Transaction Library in Create Batch pathname', async () => {
|
||||
render(<Header />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Transaction Builder')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('Your transaction library', {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
CREATE_BATCH_PATH,
|
||||
EDIT_BATCH_PATH,
|
||||
HOME_PATH,
|
||||
SAVE_BATCH_PATH,
|
||||
TRANSACTION_LIBRARY_PATH,
|
||||
} from '../routes/routes'
|
||||
import { useTransactionLibrary } from '../store'
|
||||
import ChecksumWarning from './ChecksumWarning'
|
||||
import ErrorAlert from './ErrorAlert'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import { Icon } from './Icon'
|
||||
import FixedIcon from './FixedIcon'
|
||||
import { Typography } from '@mui/material'
|
||||
import Text from './Text'
|
||||
|
||||
const HELP_ARTICLE_LINK = 'https://help.safe.global/en/articles/234052-transaction-builder'
|
||||
|
||||
const goBackLabel: Record<string, string> = {
|
||||
[CREATE_BATCH_PATH]: 'Back to Transaction Creation',
|
||||
[TRANSACTION_LIBRARY_PATH]: 'Back to Your Transaction Library',
|
||||
[EDIT_BATCH_PATH]: 'Back to Edit Batch',
|
||||
[SAVE_BATCH_PATH]: 'Back to Transaction Creation',
|
||||
}
|
||||
|
||||
type LocationType = {
|
||||
state: { from: string } | null
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
const { pathname } = useLocation()
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const goBack = () => navigate(-1)
|
||||
|
||||
const { batches } = useTransactionLibrary()
|
||||
|
||||
const isTransactionCreationPath = pathname === CREATE_BATCH_PATH
|
||||
const isSaveBatchPath = pathname === SAVE_BATCH_PATH
|
||||
|
||||
const showTitle = isTransactionCreationPath || isSaveBatchPath
|
||||
const showLinkToLibrary = isTransactionCreationPath || isSaveBatchPath
|
||||
|
||||
const { state } = useLocation() as LocationType
|
||||
|
||||
const previousUrl = state?.from || CREATE_BATCH_PATH
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
{showTitle ? (
|
||||
<>
|
||||
{/* Transaction Builder Title */}
|
||||
<StyledTitle>Transaction Builder</StyledTitle>
|
||||
<Tooltip placement="top" title="Help Article" backgroundColor="primary" arrow>
|
||||
<StyledIconLink href={HELP_ARTICLE_LINK} target="_blank" rel="noreferrer">
|
||||
<Icon size="md" type="info" />
|
||||
</StyledIconLink>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
<StyledLink to={HOME_PATH} onClick={goBack}>
|
||||
{/* Go Back link */}
|
||||
<FixedIcon type={'chevronLeft'} />
|
||||
<StyledLeftLinkLabel>{goBackLabel[previousUrl]}</StyledLeftLinkLabel>
|
||||
</StyledLink>
|
||||
)}
|
||||
|
||||
{showLinkToLibrary && (
|
||||
<RigthLinkWrapper>
|
||||
<StyledLink to={TRANSACTION_LIBRARY_PATH}>
|
||||
<StyledRightLinkLabel>{`(${batches.length}) Your transaction library`}</StyledRightLinkLabel>
|
||||
<FixedIcon type={'chevronRight'} />
|
||||
</StyledLink>
|
||||
</RigthLinkWrapper>
|
||||
)}
|
||||
</HeaderWrapper>
|
||||
<ErrorAlert />
|
||||
<ChecksumWarning />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
||||
const HeaderWrapper = styled.header`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.palette.border.light};
|
||||
z-index: 10;
|
||||
background-color: ${({ theme }) => theme.palette.background.paper};
|
||||
color: ${({ theme }) => theme.palette.text.primary};
|
||||
height: 70px;
|
||||
padding: 0 40px;
|
||||
box-sizing: border-box;
|
||||
`
|
||||
|
||||
const StyledTitle = styled(Typography)`
|
||||
&& {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.palette.common.black};
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
|
||||
> span {
|
||||
padding-top: 3px;
|
||||
|
||||
path {
|
||||
fill: ${({ theme }) => theme.palette.common.black};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLeftLinkLabel = styled(Text)`
|
||||
&& {
|
||||
margin-left: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
|
||||
const RigthLinkWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const StyledRightLinkLabel = styled(Text)`
|
||||
&& {
|
||||
font-weight: 700;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledIconLink = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
@@ -0,0 +1,34 @@
|
||||
const Alert = {
|
||||
sm: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H16V16H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M8 5.996c.553 0 1 .447 1 1v.998c0 .553-.447 1-1 1-.552 0-1-.447-1-1v-.998c0-.553.448-1 1-1M8 9.895c.607 0 1.101.492 1.101 1.1 0 .607-.494 1.1-1.1 1.1-.608 0-1.1-.493-1.1-1.1 0-.608.492-1.1 1.1-1.1"
|
||||
/>
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M8 0c-.383 0-.766.193-.975.581L.133 13.373c-.396.734.138 1.624.974 1.624h13.786c.836 0 1.37-.89.974-1.624L8.974.581C8.766.193 8.384 0 8 0m0 3l5.386 9.997H2.613L8 3"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H24V24H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M11.999 8.002c.552 0 1 .447 1 1v4c0 .552-.448 1-1 1-.553 0-1-.448-1-1v-4c0-.553.447-1 1-1M11.999 15.752c.69 0 1.25.56 1.25 1.25s-.56 1.25-1.25 1.25-1.25-.56-1.25-1.25.56-1.25 1.25-1.25"
|
||||
/>
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M12 2c-.502 0-1.004.25-1.283.752L1.187 19.83C.644 20.804 1.352 22 2.47 22H21.53c1.119 0 1.826-1.196 1.283-2.17l-9.53-17.078C13.004 2.25 12.503 2 12 2m0 2.554l8.624 15.454H3.377L12 4.554"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Alert
|
||||
@@ -0,0 +1,26 @@
|
||||
const Bookmark = {
|
||||
sm: (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.875 3.2C4.70924 3.2 4.55027 3.26321 4.43306 3.37574C4.31585 3.48826 4.25 3.64087 4.25 3.8V12.2341L7.63673 9.91176C7.85404 9.76275 8.14596 9.76275 8.36327 9.91176L11.75 12.2341V3.8C11.75 3.64087 11.6842 3.48826 11.5669 3.37574C11.4497 3.26321 11.2908 3.2 11.125 3.2H4.875ZM3.54917 2.52721C3.90081 2.18964 4.37772 2 4.875 2H11.125C11.6223 2 12.0992 2.18964 12.4508 2.52721C12.8025 2.86477 13 3.32261 13 3.8V13.4C13 13.6248 12.8692 13.8307 12.661 13.9335C12.4528 14.0363 12.2022 14.0189 12.0117 13.8882L8 11.1373L3.98827 13.8882C3.79776 14.0189 3.54718 14.0363 3.33901 13.9335C3.13084 13.8307 3 13.6248 3 13.4V3.8C3 3.32261 3.19754 2.86477 3.54917 2.52721Z"
|
||||
fill="black"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.875 3.2C4.70924 3.2 4.55027 3.26321 4.43306 3.37574C4.31585 3.48826 4.25 3.64087 4.25 3.8V12.2341L7.63673 9.91176C7.85404 9.76275 8.14596 9.76275 8.36327 9.91176L11.75 12.2341V3.8C11.75 3.64087 11.6842 3.48826 11.5669 3.37574C11.4497 3.26321 11.2908 3.2 11.125 3.2H4.875ZM3.54917 2.52721C3.90081 2.18964 4.37772 2 4.875 2H11.125C11.6223 2 12.0992 2.18964 12.4508 2.52721C12.8025 2.86477 13 3.32261 13 3.8V13.4C13 13.6248 12.8692 13.8307 12.661 13.9335C12.4528 14.0363 12.2022 14.0189 12.0117 13.8882L8 11.1373L3.98827 13.8882C3.79776 14.0189 3.54718 14.0363 3.33901 13.9335C3.13084 13.8307 3 13.6248 3 13.4V3.8C3 3.32261 3.19754 2.86477 3.54917 2.52721Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Bookmark
|
||||
@@ -0,0 +1,30 @@
|
||||
const BookMarkFilled = {
|
||||
sm: (
|
||||
<svg height="16" width="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
className="icon-color icon-stroke"
|
||||
d="M9 11L5 8.22222L1 11V2.11111C1 1.81643 1.12041 1.53381 1.33474 1.32544C1.54906 1.11706 1.83975 1 2.14286 1H7.85714C8.16025 1 8.45094 1.11706 8.66527 1.32544C8.87959 1.53381 9 1.81643 9 2.11111V11Z"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg height="24" width="20" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
className="icon-color icon-stroke"
|
||||
d="M9 11L5 8.22222L1 11V2.11111C1 1.81643 1.12041 1.53381 1.33474 1.32544C1.54906 1.11706 1.83975 1 2.14286 1H7.85714C8.16025 1 8.45094 1.11706 8.66527 1.32544C8.87959 1.53381 9 1.81643 9 2.11111V11Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default BookMarkFilled
|
||||
@@ -0,0 +1,26 @@
|
||||
const Check = {
|
||||
sm: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H16V16H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M6 13.003c-.267 0-.522-.105-.709-.293L1.292 8.706c-.39-.39-.39-1.024.001-1.415.391-.389 1.024-.39 1.415.001L6 10.588l7.296-7.295c.391-.39 1.023-.39 1.414 0 .391.391.391 1.023 0 1.414L6.706 12.71c-.187.188-.44.293-.707.293"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H24V24H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M8.999 19c-.266 0-.52-.105-.707-.293l-5.999-6.003c-.391-.391-.391-1.024 0-1.414.391-.391 1.023-.391 1.414 0l5.292 5.296L20.291 5.293c.391-.391 1.023-.391 1.414 0s.391 1.023 0 1.414l-11.999 12c-.187.188-.441.293-.707.293"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Check
|
||||
@@ -0,0 +1,27 @@
|
||||
const Code = {
|
||||
sm: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H16V16H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M3.413 7.994l3.293-3.286c.391-.39.392-1.023.002-1.414s-1.022-.392-1.413-.002L1.294 7.284c-.391.39-.392 1.024-.002 1.415l4.002 4.01c.39.391 1.022.392 1.413.002.39-.39.39-1.024 0-1.415L3.414 7.994zM12.585 7.995l-3.292 3.297c-.39.391-.39 1.025 0 1.415.391.39 1.024.39 1.414 0l4-4.006c.39-.39.39-1.024-.001-1.415l-3.993-3.993c-.39-.39-1.024-.39-1.414 0-.39.391-.39 1.025 0 1.415l3.286 3.287z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H24V24H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M9 19c-.255 0-.511-.099-.706-.293l-6.001-5.999C2.106 12.52 2 12.266 2 12c0-.265.106-.52.293-.707l5.996-6c.39-.39 1.023-.39 1.413 0 .39.391.39 1.023 0 1.414L4.412 12l5.295 5.291c.39.391.39 1.023 0 1.415-.194.195-.45.293-.706.293M15 19c-.256 0-.512-.099-.707-.294-.39-.39-.39-1.023 0-1.414L19.585 12l-5.292-5.293c-.39-.39-.39-1.023 0-1.414.391-.39 1.023-.39 1.414 0l6 6c.39.391.39 1.023 0 1.414l-6 6c-.195.194-.45.292-.707.292"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Code
|
||||
@@ -0,0 +1,28 @@
|
||||
const Copy = {
|
||||
sm: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H16V16H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M10 10h3V3H8v1c1.105 0 2 .895 2 2v4zM6 4V3c0-1.105.895-2 2-2h5c1.105 0 2 .895 2 2v7c0 1.105-.895 2-2 2h-3v1c0 1.105-.895 2-2 2H3c-1.105 0-2-.895-2-2V6c0-1.105.895-2 2-2h3zM3 6v7h5V6H3z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H24V24H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M15 15h4V4h-8v3h2c1.105 0 2 .895 2 2v6zM9 7V4c0-1.105.895-2 2-2h8c1.105 0 2 .895 2 2v11c0 1.105-.895 2-2 2h-4v3c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V9c0-1.105.895-2 2-2h4zM5 9v11h8V9H5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Copy
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
|
||||
const Cross = {
|
||||
sm: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H16V16H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M9.413 8l5.293 5.292c.39.39.39 1.025 0 1.414-.39.39-1.025.39-1.414 0L8 9.413l-5.293 5.293c-.39.39-1.025.39-1.414 0-.39-.389-.39-1.025 0-1.414L6.585 8 1.292 2.706c-.39-.389-.39-1.025 0-1.414.39-.39 1.024-.39 1.414 0L8 6.585l5.293-5.293c.39-.39 1.024-.39 1.414 0 .39.39.39 1.025 0 1.414L9.413 8z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H24V24H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M13.413 11.998l8.291 8.291c.39.39.39 1.025 0 1.414-.39.39-1.025.39-1.415 0l-8.291-8.29-8.292 8.29c-.39.39-1.025.39-1.415 0-.389-.389-.389-1.024 0-1.414l8.292-8.291-8.292-8.292c-.389-.39-.389-1.025 0-1.415.39-.389 1.025-.389 1.415 0l8.292 8.292 8.291-8.292c.39-.389 1.025-.389 1.415 0 .39.39.39 1.025 0 1.415l-8.291 8.292z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Cross
|
||||
@@ -0,0 +1,54 @@
|
||||
const Delete = {
|
||||
sm: (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M6.66667 12.0064C7.03533 12.0064 7.33333 11.7084 7.33333 11.3398V7.33976C7.33333 6.9711 7.03533 6.6731 6.66667 6.6731C6.298 6.6731 6 6.9711 6 7.33976V11.3398C6 11.7084 6.298 12.0064 6.66667 12.0064Z"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M9.3339 12.0064C9.70257 12.0064 10.0006 11.7084 10.0006 11.3398V7.33976C10.0006 6.9711 9.70257 6.6731 9.3339 6.6731C8.96524 6.6731 8.66724 6.9711 8.66724 7.33976V11.3398C8.66724 11.7084 8.96524 12.0064 9.3339 12.0064Z"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M11.0084 13.0339C10.9998 13.2033 10.8364 13.3359 10.6771 13.3346H5.31242C5.15909 13.3173 5.00509 13.2039 4.99642 13.0293L4.56442 5.33325H11.4224L11.0084 13.0339ZM6.66442 3.07725C6.66442 2.85459 6.85309 2.66659 7.07576 2.66659H8.92109C9.14776 2.66659 9.33242 2.85125 9.33242 3.07725V3.99992H6.66442V3.07725ZM13.3331 3.99992H12.1744C12.1704 3.99992 12.1664 3.99659 12.1618 3.99659C12.1538 3.99592 12.1471 3.99992 12.1384 3.99992H10.6658V3.07725C10.6658 2.11592 9.88376 1.33325 8.92109 1.33325H7.07576C6.11376 1.33325 5.33109 2.11592 5.33109 3.07725V3.99992H2.66642C2.29842 3.99992 1.99976 4.29792 1.99976 4.66658C1.99976 5.03525 2.29842 5.33325 2.66642 5.33325H3.22976L3.66509 13.0979C3.70709 13.9713 4.43909 14.6686 5.29176 14.6686C5.30376 14.6686 5.31576 14.6679 5.32776 14.6679H10.6611H10.6991C11.5658 14.6679 12.2984 13.9719 12.3404 13.1026L12.7578 5.33325H13.3331C13.7018 5.33325 13.9998 5.03525 13.9998 4.66658C13.9998 4.29792 13.7018 3.99992 13.3331 3.99992Z"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M6.66667 12.0064C7.03533 12.0064 7.33333 11.7084 7.33333 11.3398V7.33976C7.33333 6.9711 7.03533 6.6731 6.66667 6.6731C6.298 6.6731 6 6.9711 6 7.33976V11.3398C6 11.7084 6.298 12.0064 6.66667 12.0064Z"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M9.3339 12.0064C9.70257 12.0064 10.0006 11.7084 10.0006 11.3398V7.33976C10.0006 6.9711 9.70257 6.6731 9.3339 6.6731C8.96524 6.6731 8.66724 6.9711 8.66724 7.33976V11.3398C8.66724 11.7084 8.96524 12.0064 9.3339 12.0064Z"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M11.0084 13.0339C10.9998 13.2033 10.8364 13.3359 10.6771 13.3346H5.31242C5.15909 13.3173 5.00509 13.2039 4.99642 13.0293L4.56442 5.33325H11.4224L11.0084 13.0339ZM6.66442 3.07725C6.66442 2.85459 6.85309 2.66659 7.07576 2.66659H8.92109C9.14776 2.66659 9.33242 2.85125 9.33242 3.07725V3.99992H6.66442V3.07725ZM13.3331 3.99992H12.1744C12.1704 3.99992 12.1664 3.99659 12.1618 3.99659C12.1538 3.99592 12.1471 3.99992 12.1384 3.99992H10.6658V3.07725C10.6658 2.11592 9.88376 1.33325 8.92109 1.33325H7.07576C6.11376 1.33325 5.33109 2.11592 5.33109 3.07725V3.99992H2.66642C2.29842 3.99992 1.99976 4.29792 1.99976 4.66658C1.99976 5.03525 2.29842 5.33325 2.66642 5.33325H3.22976L3.66509 13.0979C3.70709 13.9713 4.43909 14.6686 5.29176 14.6686C5.30376 14.6686 5.31576 14.6679 5.32776 14.6679H10.6611H10.6991C11.5658 14.6679 12.2984 13.9719 12.3404 13.1026L12.7578 5.33325H13.3331C13.7018 5.33325 13.9998 5.03525 13.9998 4.66658C13.9998 4.29792 13.7018 3.99992 13.3331 3.99992Z"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Delete
|
||||
@@ -0,0 +1,26 @@
|
||||
const Edit = {
|
||||
sm: (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.7286 6.02792L10.0079 4.29259L11.6539 2.67925L13.3186 4.35859C13.3226 4.36259 13.3153 4.42659 13.3199 4.43059L11.7286 6.02792ZM4.39325 13.3333H2.66659V11.5859L9.05658 5.22725L10.7866 6.97192L4.39325 13.3333ZM14.6666 4.37125C14.6619 4.00059 14.5146 3.65792 14.2653 3.41992L12.6126 1.75325C12.3666 1.49059 12.0159 1.33792 11.6493 1.33325H11.6313C11.2639 1.33325 10.9079 1.48125 10.6526 1.73792L1.52992 10.8359C1.40459 10.9606 1.33325 11.1313 1.33325 11.3086V13.9999C1.33325 14.3686 1.63125 14.6666 1.99992 14.6666H4.66859C4.84525 14.6666 5.01392 14.5973 5.13859 14.4726L14.2653 5.37125C14.5253 5.10859 14.6719 4.74392 14.6666 4.37125Z"
|
||||
fill="#A1A3A7"
|
||||
className="icon-color"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
className="icon-color"
|
||||
d="M11.7286 6.02792L10.0079 4.29259L11.6539 2.67925L13.3186 4.35859C13.3226 4.36259 13.3153 4.42659 13.3199 4.43059L11.7286 6.02792ZM4.39325 13.3333H2.66659V11.5859L9.05658 5.22725L10.7866 6.97192L4.39325 13.3333ZM14.6666 4.37125C14.6619 4.00059 14.5146 3.65792 14.2653 3.41992L12.6126 1.75325C12.3666 1.49059 12.0159 1.33792 11.6493 1.33325H11.6313C11.2639 1.33325 10.9079 1.48125 10.6526 1.73792L1.52992 10.8359C1.40459 10.9606 1.33325 11.1313 1.33325 11.3086V13.9999C1.33325 14.3686 1.63125 14.6666 1.99992 14.6666H4.66859C4.84525 14.6666 5.01392 14.5973 5.13859 14.4726L14.2653 5.37125C14.5253 5.10859 14.6719 4.74392 14.6666 4.37125Z"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Edit
|
||||
@@ -0,0 +1,36 @@
|
||||
const ExternalLink = {
|
||||
sm: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H16V16H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M13 13v-2c0-.552.448-1 1-1s1 .448 1 1v2c0 1.105-.895 2-2 2H3c-1.105 0-2-.895-2-2V3c0-1.105.895-2 2-2h2c.552 0 1 .448 1 1s-.448 1-1 1H3v10h10z"
|
||||
/>
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M11.586 3H9c-.552 0-1-.448-1-1s.448-1 1-1h5c.276 0 .526.112.707.293.181.18.293.43.293.707v5c0 .552-.448 1-1 1s-1-.448-1-1V4.414l-6.243 6.243c-.39.39-1.023.39-1.414 0-.39-.39-.39-1.024 0-1.414L11.586 3z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H24V24H0z" />
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M20 20v-8c0-.552.448-1 1-1s1 .448 1 1v8c0 1.105-.895 2-2 2H4c-1.105 0-2-.895-2-2V4c0-1.105.895-2 2-2h8c.552 0 1 .448 1 1s-.448 1-1 1H4v16h16z"
|
||||
/>
|
||||
<path
|
||||
className="icon-color"
|
||||
d="M18.536 4H15.95c-.553 0-1-.448-1-1s.447-1 1-1h5c.276 0 .526.112.707.293.18.18.293.43.293.707v5c0 .552-.448 1-1 1-.553 0-1-.448-1-1V5.414l-9.243 9.243c-.39.39-1.024.39-1.414 0-.39-.39-.39-1.024 0-1.414L18.536 4z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default ExternalLink
|
||||
@@ -0,0 +1,40 @@
|
||||
const Import = {
|
||||
sm: (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.66699 10.3893L8.66699 2.00062C8.66699 1.63195 8.36832 1.33395 8.00032 1.33395C7.63166 1.33395 7.33366 1.63195 7.33366 2.00062L7.33366 10.3893L4.58566 7.64195C4.32499 7.38128 3.90299 7.38128 3.64299 7.64195C3.38232 7.90262 3.38232 8.32395 3.64299 8.58462L7.41432 12.3559C7.43366 12.3753 7.45432 12.3933 7.47499 12.4093C7.59766 12.5659 7.78699 12.6653 8.00032 12.6653C8.21299 12.6653 8.40299 12.5659 8.52499 12.4093C8.54566 12.3933 8.56699 12.3753 8.58566 12.3559L12.357 8.58462C12.6177 8.32395 12.6177 7.90262 12.357 7.64195C12.097 7.38128 11.675 7.38128 11.4143 7.64195L8.66699 10.3893Z"
|
||||
className="icon-color"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.00033 13.3334L14.0003 13.3334C14.367 13.3334 14.667 13.6334 14.667 14.0001C14.667 14.3667 14.367 14.6667 14.0003 14.6667L2.00033 14.6667C1.63299 14.6667 1.33366 14.3667 1.33366 14.0001C1.33366 13.6334 1.63299 13.3334 2.00033 13.3334Z"
|
||||
className="icon-color"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.66699 10.3893L8.66699 2.00062C8.66699 1.63195 8.36832 1.33395 8.00032 1.33395C7.63166 1.33395 7.33366 1.63195 7.33366 2.00062L7.33366 10.3893L4.58566 7.64195C4.32499 7.38128 3.90299 7.38128 3.64299 7.64195C3.38232 7.90262 3.38232 8.32395 3.64299 8.58462L7.41432 12.3559C7.43366 12.3753 7.45432 12.3933 7.47499 12.4093C7.59766 12.5659 7.78699 12.6653 8.00032 12.6653C8.21299 12.6653 8.40299 12.5659 8.52499 12.4093C8.54566 12.3933 8.56699 12.3753 8.58566 12.3559L12.357 8.58462C12.6177 8.32395 12.6177 7.90262 12.357 7.64195C12.097 7.38128 11.675 7.38128 11.4143 7.64195L8.66699 10.3893Z"
|
||||
className="icon-color"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
className="icon-color"
|
||||
clipRule="evenodd"
|
||||
d="M2.00033 13.3334L14.0003 13.3334C14.367 13.3334 14.667 13.6334 14.667 14.0001C14.667 14.3667 14.367 14.6667 14.0003 14.6667L2.00033 14.6667C1.63299 14.6667 1.33366 14.3667 1.33366 14.0001C1.33366 13.6334 1.63299 13.3334 2.00033 13.3334Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Import
|
||||
@@ -0,0 +1,40 @@
|
||||
const Info = {
|
||||
sm: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H16V16H0z" />
|
||||
<rect width="2" height="5" x="7" y="7" className="icon-color" rx="1" />
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M8 3.9c.608 0 1.1.492 1.1 1.1 0 .608-.492 1.1-1.1 1.1-.608 0-1.1-.492-1.1-1.1 0-.608.492-1.1 1.1-1.1z"
|
||||
/>
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M8 15c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm0-2c-2.761 0-5-2.239-5-5s2.239-5 5-5 5 2.239 5 5-2.239 5-5 5z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0H24V24H0z" />
|
||||
<rect width="2" height="8" x="11" y="10" className="icon-color" rx="1" />
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M12 5.75c.69 0 1.25.56 1.25 1.25S12.69 8.25 12 8.25 10.75 7.69 10.75 7s.56-1.25 1.25-1.25z"
|
||||
/>
|
||||
<path
|
||||
className="icon-color"
|
||||
fillRule="nonzero"
|
||||
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm0-2c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default Info
|
||||
@@ -0,0 +1,26 @@
|
||||
const TermsOfUse = {
|
||||
sm: (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.50153 1.53477C3.82266 1.19235 4.2582 1 4.71233 1H9.09587C9.25936 1 9.41615 1.06925 9.53176 1.19252L12.8194 4.69815C12.9351 4.82142 13 4.9886 13 5.16293V12.1742C13 12.6583 12.8196 13.1228 12.4985 13.4651C12.4985 13.4652 12.4984 13.4652 12.4984 13.4652C12.4984 13.4653 12.4984 13.4653 12.4983 13.4653C12.1771 13.8079 11.7415 14 11.2876 14H4.71233C4.25827 14 3.82268 13.8078 3.50148 13.4652C3.18035 13.1228 3 12.6583 3 12.1742V2.82585C3 2.3416 3.18041 1.87719 3.50152 1.53478C3.50153 1.53478 3.50153 1.53478 3.50153 1.53477ZM4.71233 2.31461C4.58515 2.31461 4.46321 2.36848 4.37331 2.46434L4.3733 2.46434C4.28339 2.56023 4.23287 2.69026 4.23287 2.82585V12.1742C4.23287 12.3098 4.28341 12.4398 4.37326 12.5356L4.37336 12.5357C4.46318 12.6315 4.58508 12.6854 4.71233 12.6854H11.2876C11.4149 12.6854 11.5368 12.6315 11.6266 12.5358L11.6267 12.5356C11.7166 12.4398 11.7671 12.3097 11.7671 12.1742V5.82024H9.09533C8.93183 5.82024 8.77502 5.75097 8.65941 5.62768C8.54381 5.50439 8.47887 5.33717 8.4789 5.16283L8.47933 2.31461H4.71233ZM9.71206 3.24391L10.8953 4.50563H9.71187L9.71206 3.24391ZM5.19204 5.74749C5.19204 5.38447 5.46803 5.09018 5.80848 5.09018H6.90437C7.24482 5.09018 7.5208 5.38447 7.5208 5.74749C7.5208 6.11051 7.24482 6.4048 6.90437 6.4048H5.80848C5.46803 6.4048 5.19204 6.11051 5.19204 5.74749ZM5.19124 8.08486C5.19124 7.72184 5.46723 7.42756 5.80767 7.42756H10.1912C10.5316 7.42756 10.8076 7.72184 10.8076 8.08486C10.8076 8.44788 10.5316 8.74217 10.1912 8.74217H5.80767C5.46723 8.74217 5.19124 8.44788 5.19124 8.08486ZM5.19124 10.4214C5.19124 10.0584 5.46723 9.76407 5.80767 9.76407H10.1912C10.5316 9.76407 10.8076 10.0584 10.8076 10.4214C10.8076 10.7844 10.5316 11.0787 10.1912 11.0787H5.80767C5.46723 11.0787 5.19124 10.7844 5.19124 10.4214Z"
|
||||
className="icon-color"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
md: (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.50153 1.53477C3.82266 1.19235 4.2582 1 4.71233 1H9.09587C9.25936 1 9.41615 1.06925 9.53176 1.19252L12.8194 4.69815C12.9351 4.82142 13 4.9886 13 5.16293V12.1742C13 12.6583 12.8196 13.1228 12.4985 13.4651C12.4985 13.4652 12.4984 13.4652 12.4984 13.4652C12.4984 13.4653 12.4984 13.4653 12.4983 13.4653C12.1771 13.8079 11.7415 14 11.2876 14H4.71233C4.25827 14 3.82268 13.8078 3.50148 13.4652C3.18035 13.1228 3 12.6583 3 12.1742V2.82585C3 2.3416 3.18041 1.87719 3.50152 1.53478C3.50153 1.53478 3.50153 1.53478 3.50153 1.53477ZM4.71233 2.31461C4.58515 2.31461 4.46321 2.36848 4.37331 2.46434L4.3733 2.46434C4.28339 2.56023 4.23287 2.69026 4.23287 2.82585V12.1742C4.23287 12.3098 4.28341 12.4398 4.37326 12.5356L4.37336 12.5357C4.46318 12.6315 4.58508 12.6854 4.71233 12.6854H11.2876C11.4149 12.6854 11.5368 12.6315 11.6266 12.5358L11.6267 12.5356C11.7166 12.4398 11.7671 12.3097 11.7671 12.1742V5.82024H9.09533C8.93183 5.82024 8.77502 5.75097 8.65941 5.62768C8.54381 5.50439 8.47887 5.33717 8.4789 5.16283L8.47933 2.31461H4.71233ZM9.71206 3.24391L10.8953 4.50563H9.71187L9.71206 3.24391ZM5.19204 5.74749C5.19204 5.38447 5.46803 5.09018 5.80848 5.09018H6.90437C7.24482 5.09018 7.5208 5.38447 7.5208 5.74749C7.5208 6.11051 7.24482 6.4048 6.90437 6.4048H5.80848C5.46803 6.4048 5.19204 6.11051 5.19204 5.74749ZM5.19124 8.08486C5.19124 7.72184 5.46723 7.42756 5.80767 7.42756H10.1912C10.5316 7.42756 10.8076 7.72184 10.8076 8.08486C10.8076 8.44788 10.5316 8.74217 10.1912 8.74217H5.80767C5.46723 8.74217 5.19124 8.44788 5.19124 8.08486ZM5.19124 10.4214C5.19124 10.0584 5.46723 9.76407 5.80767 9.76407H10.1912C10.5316 9.76407 10.8076 10.0584 10.8076 10.4214C10.8076 10.7844 10.5316 11.0787 10.1912 11.0787H5.80767C5.46723 11.0787 5.19124 10.7844 5.19124 10.4214Z"
|
||||
className="icon-color"
|
||||
fill="#A1A3A7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export default TermsOfUse
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { type Theme } from '@mui/material/styles'
|
||||
import { Tooltip } from '../Tooltip'
|
||||
|
||||
import alert from './images/alert'
|
||||
import bookmark from './images/bookmark'
|
||||
import bookmarkFilled from './images/bookmarkFilled'
|
||||
import check from './images/check'
|
||||
import code from './images/code'
|
||||
import copy from './images/copy'
|
||||
import cross from './images/cross'
|
||||
import deleteIcon from './images/delete'
|
||||
import edit from './images/edit'
|
||||
import externalLink from './images/externalLink'
|
||||
import importImg from './images/import'
|
||||
import info from './images/info'
|
||||
import termsOfUse from './images/termsOfUse'
|
||||
|
||||
const StyledIcon = styled.span<{ color?: keyof Theme['palette'] }>`
|
||||
display: inline-flex;
|
||||
|
||||
.icon-color {
|
||||
fill: ${({ theme, color }) => (color ? theme.palette[color].main : '#B2B5B2')};
|
||||
}
|
||||
|
||||
.icon-stroke {
|
||||
stroke: ${({ theme, color }) => (color ? theme.palette[color].main : '#B2B5B2')};
|
||||
}
|
||||
`
|
||||
|
||||
const icons = {
|
||||
alert,
|
||||
bookmark,
|
||||
bookmarkFilled,
|
||||
check,
|
||||
copy,
|
||||
code,
|
||||
cross,
|
||||
delete: deleteIcon,
|
||||
edit,
|
||||
externalLink,
|
||||
importImg,
|
||||
info,
|
||||
termsOfUse,
|
||||
}
|
||||
|
||||
export type IconType = typeof icons
|
||||
export type IconTypes = keyof IconType
|
||||
|
||||
export type IconProps = {
|
||||
type: IconTypes
|
||||
size: 'sm' | 'md'
|
||||
color?: keyof Theme['palette']
|
||||
tooltip?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Icon` renders an icon, it can be one already defined specified by
|
||||
* the type Iconprops or custom one using the customUrl.
|
||||
*/
|
||||
export const Icon = ({ type, size, color, tooltip, className }: IconProps): React.ReactElement => {
|
||||
const IconElement = (
|
||||
<StyledIcon color={color} className={className}>
|
||||
{icons[type][size]}
|
||||
</StyledIcon>
|
||||
)
|
||||
return tooltip === undefined ? (
|
||||
IconElement
|
||||
) : (
|
||||
<Tooltip title={tooltip} placement="top">
|
||||
{IconElement}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { type Theme } from '@mui/material/styles'
|
||||
|
||||
import { Icon, IconProps, IconType } from '../Icon'
|
||||
import Text from '../Text'
|
||||
|
||||
const iconTextMargins = {
|
||||
xxs: '4px',
|
||||
xs: '6px',
|
||||
sm: '8px',
|
||||
md: '12px',
|
||||
lg: '16px',
|
||||
xl: '20px',
|
||||
xxl: '24px',
|
||||
}
|
||||
|
||||
type IconMargins = keyof typeof iconTextMargins
|
||||
|
||||
type Props = {
|
||||
iconType: keyof IconType
|
||||
iconSize: IconProps['size']
|
||||
iconColor?: keyof Theme['palette']
|
||||
margin?: IconMargins
|
||||
color?: keyof Theme['palette']
|
||||
text: string
|
||||
className?: string
|
||||
iconSide?: 'left' | 'right'
|
||||
}
|
||||
|
||||
const LeftIconText = styled.div<{ margin: IconMargins }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
margin: 0 ${({ margin }) => iconTextMargins[margin]} 0 0;
|
||||
}
|
||||
`
|
||||
|
||||
const RightIconText = styled.div<{ margin: IconMargins }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
margin: 0 0 0 ${({ margin }) => iconTextMargins[margin]};
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* The `IconText` renders an icon next to a text
|
||||
*/
|
||||
const IconText = ({
|
||||
iconSize,
|
||||
margin = 'xs',
|
||||
iconType,
|
||||
iconColor,
|
||||
text,
|
||||
iconSide = 'left',
|
||||
color,
|
||||
className,
|
||||
}: Props): React.ReactElement => {
|
||||
return iconSide === 'right' ? (
|
||||
<RightIconText className={className} margin={margin}>
|
||||
<Text color={color}>{text}</Text>
|
||||
<Icon size={iconSize} type={iconType} color={iconColor} />
|
||||
</RightIconText>
|
||||
) : (
|
||||
<LeftIconText className={className} margin={margin}>
|
||||
<Icon size={iconSize} type={iconType} color={iconColor} />
|
||||
<Text color={color}>{text}</Text>
|
||||
</LeftIconText>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconText
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { type Theme } from '@mui/material/styles'
|
||||
|
||||
export interface Props extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
color?: keyof Theme['palette'] | 'white'
|
||||
}
|
||||
|
||||
const StyledLink = styled.a<Props>`
|
||||
cursor: pointer;
|
||||
color: ${({ theme, color = 'primary' }) =>
|
||||
color === 'white' ? theme.palette.common.white : theme.palette[color].dark};
|
||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
||||
text-decoration: underline;
|
||||
`
|
||||
|
||||
const Link: React.FC<Props> = ({ children, ...rest }): React.ReactElement => {
|
||||
return <StyledLink {...rest}>{children}</StyledLink>
|
||||
}
|
||||
|
||||
export default Link
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import { type Theme } from '@mui/material/styles'
|
||||
|
||||
const loaderSizes = {
|
||||
xxs: '10px',
|
||||
xs: '16px',
|
||||
sm: '30px',
|
||||
md: '50px',
|
||||
lg: '70px',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
size: keyof typeof loaderSizes
|
||||
color?: keyof Theme['palette']
|
||||
className?: string
|
||||
}
|
||||
|
||||
const StyledCircularProgress = styled(
|
||||
({ size, className }: Props): React.ReactElement => (
|
||||
<CircularProgress size={loaderSizes[size]} className={className} />
|
||||
),
|
||||
)`
|
||||
&.MuiCircularProgress-colorPrimary {
|
||||
color: ${({ theme, color = 'primary' }) => theme.palette[color].main};
|
||||
}
|
||||
`
|
||||
|
||||
const Loader = ({ className, size, color }: Props): React.ReactElement => (
|
||||
<StyledCircularProgress size={size} color={color} className={className} />
|
||||
)
|
||||
|
||||
export default Loader
|
||||
@@ -0,0 +1,48 @@
|
||||
import MuiAlert from '@mui/lab/Alert'
|
||||
import MuiAlertTitle from '@mui/lab/AlertTitle'
|
||||
import styled from 'styled-components'
|
||||
import { Icon } from './Icon'
|
||||
|
||||
type QuickTipProps = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const QuickTip = ({ onClose }: QuickTipProps) => {
|
||||
return (
|
||||
<StyledAlert severity="success" onClose={onClose} icon={false}>
|
||||
<StyledTitle>Quick Tip</StyledTitle>
|
||||
You can save your batches in your transaction library{' '}
|
||||
<StyledIcon size="sm" type="bookmark" color="primary" aria-label="Save to Library" /> (local browser storage) or{' '}
|
||||
<StyledIcon size="sm" type="importImg" color="primary" aria-label="Download" /> download the .json file to use
|
||||
them later.
|
||||
</StyledAlert>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledAlert = styled(MuiAlert)`
|
||||
&& {
|
||||
font-size: 14px;
|
||||
padding: 24px;
|
||||
background: ${({ theme }) => theme.palette.secondary.background};
|
||||
color: ${({ theme }) => theme.palette.text.primary};
|
||||
border-radius: 8px;
|
||||
|
||||
.MuiAlert-action {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StyledTitle = styled(MuiAlertTitle)`
|
||||
&& {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
position: relative;
|
||||
top: 3px;
|
||||
`
|
||||
|
||||
export default QuickTip
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState, SyntheticEvent } from 'react'
|
||||
import Link from './Link'
|
||||
|
||||
type ShowMoreTextProps = {
|
||||
children: string
|
||||
moreLabel?: string
|
||||
lessLabel?: string
|
||||
splitIndex?: number
|
||||
}
|
||||
|
||||
const SHOW_MORE = 'Show more'
|
||||
const SHOW_LESS = 'Show less'
|
||||
|
||||
export const ShowMoreText = ({
|
||||
children,
|
||||
moreLabel = SHOW_MORE,
|
||||
lessLabel = SHOW_LESS,
|
||||
splitIndex = 50,
|
||||
}: ShowMoreTextProps) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const handleToggle = (event: SyntheticEvent) => {
|
||||
event.preventDefault()
|
||||
setExpanded(!expanded)
|
||||
}
|
||||
|
||||
if (children.length < splitIndex) {
|
||||
return <span>{children}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{expanded ? `${children} ` : `${children.substr(0, splitIndex)} ... `}
|
||||
<Link onClick={handleToggle}>{expanded ? lessLabel : moreLabel}</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
import SwitchMui from '@mui/material/Switch'
|
||||
import styled from 'styled-components'
|
||||
import { alpha } from '@mui/material/styles'
|
||||
|
||||
const StyledSwitch = styled(({ ...rest }) => <SwitchMui {...rest} />)`
|
||||
&& {
|
||||
.MuiSwitch-thumb {
|
||||
background: ${({ theme, checked }) => (checked ? '#12FF80' : theme.palette.common.white)};
|
||||
box-shadow:
|
||||
1px 1px 2px rgba(0, 0, 0, 0.2),
|
||||
0 0 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.MuiSwitch-track {
|
||||
background: ${({ theme }) => theme.palette.common.black};
|
||||
}
|
||||
|
||||
.MuiIconButton-label,
|
||||
.MuiSwitch-colorSecondary.Mui-checked {
|
||||
color: ${({ checked, theme }) => (checked ? theme.palette.secondary.dark : '#B2B5B2')};
|
||||
}
|
||||
|
||||
.MuiSwitch-colorSecondary.Mui-checked:hover {
|
||||
background-color: ${({ theme }) => alpha(theme.palette.secondary.dark, 0.08)};
|
||||
}
|
||||
|
||||
.Mui-checked + .MuiSwitch-track {
|
||||
background-color: ${({ theme }) => theme.palette.secondary.dark};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
}
|
||||
|
||||
const Switch = ({ checked, onChange }: Props): React.ReactElement => {
|
||||
const onSwitchChange = (_event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => onChange(checked)
|
||||
|
||||
return <StyledSwitch checked={checked} onChange={onSwitchChange} />
|
||||
}
|
||||
|
||||
export default Switch
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import MuiTooltip from '@mui/material/Tooltip'
|
||||
import { alpha, styled as muiStyled, type Theme } from '@mui/material/styles'
|
||||
import { Typography, TypographyProps } from '@mui/material'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
tooltip?: string
|
||||
color?: keyof Theme['palette'] | 'white'
|
||||
className?: string
|
||||
component?: 'span' | 'p'
|
||||
strong?: boolean
|
||||
center?: boolean
|
||||
}
|
||||
|
||||
const StyledTooltip = muiStyled(MuiTooltip)(({ theme }) => ({
|
||||
'& .MuiTooltip-tooltip': {
|
||||
backgroundColor: theme.palette.common.white,
|
||||
color: theme.palette.text.primary,
|
||||
boxShadow: `0px 0px 10px ${alpha('#28363D', 0.2)}`,
|
||||
},
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: theme.palette.common.white,
|
||||
},
|
||||
}))
|
||||
|
||||
const Text = ({
|
||||
children,
|
||||
component = 'p',
|
||||
tooltip,
|
||||
color,
|
||||
strong,
|
||||
center,
|
||||
className,
|
||||
...rest
|
||||
}: Props & Omit<TypographyProps, 'color'>): React.ReactElement => {
|
||||
const textColor = color ? (color === 'white' ? 'common.white' : `${color}.main`) : 'text.primary'
|
||||
|
||||
const TextElement = (
|
||||
<Typography
|
||||
component={component}
|
||||
className={className}
|
||||
sx={{
|
||||
color: textColor,
|
||||
textAlign: center ? 'center' : undefined,
|
||||
fontWeight: strong ? 'bold' : undefined,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
)
|
||||
|
||||
return tooltip === undefined ? (
|
||||
TextElement
|
||||
) : (
|
||||
<StyledTooltip title={tooltip} placement="bottom" arrow>
|
||||
{TextElement}
|
||||
</StyledTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default Text
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
type Props = {
|
||||
children: string | React.ReactNode
|
||||
size: SizeType
|
||||
withoutMargin?: boolean
|
||||
strong?: boolean
|
||||
}
|
||||
|
||||
const StyledH1 = styled.h1<{ $withoutMargin?: boolean; $strong?: boolean }>`
|
||||
font-family: ${({ theme }) => theme.legacy?.fonts?.fontFamily || theme.typography?.fontFamily};
|
||||
font-size: ${({ theme }) => theme.legacy?.title?.size?.xl?.fontSize || '32px'};
|
||||
line-height: ${({ theme }) => theme.legacy?.title?.size?.xl?.lineHeight || '40px'};
|
||||
font-weight: ${({ $strong }) => ($strong ? 'bold' : 'normal')};
|
||||
margin: ${({ $withoutMargin }) => ($withoutMargin ? 0 : '30px')} 0;
|
||||
`
|
||||
|
||||
const StyledH2 = styled.h2<{ $withoutMargin?: boolean; $strong?: boolean }>`
|
||||
font-family: ${({ theme }) => theme.legacy?.fonts?.fontFamily || theme.typography?.fontFamily};
|
||||
font-size: ${({ theme }) => theme.legacy?.title?.size?.lg?.fontSize || '24px'};
|
||||
line-height: ${({ theme }) => theme.legacy?.title?.size?.lg?.lineHeight || '32px'};
|
||||
font-weight: ${({ $strong }) => ($strong ? 'bold' : 'normal')};
|
||||
margin: ${({ $withoutMargin }) => ($withoutMargin ? 0 : '28px')} 0;
|
||||
`
|
||||
|
||||
const StyledH3 = styled.h3<{ $withoutMargin?: boolean; $strong?: boolean }>`
|
||||
font-family: ${({ theme }) => theme.legacy?.fonts?.fontFamily || theme.typography?.fontFamily};
|
||||
font-size: ${({ theme }) => theme.legacy?.title?.size?.md?.fontSize || '20px'};
|
||||
line-height: ${({ theme }) => theme.legacy?.title?.size?.md?.lineHeight || '28px'};
|
||||
font-weight: ${({ $strong }) => ($strong ? 'bold' : 'normal')};
|
||||
margin: ${({ $withoutMargin }) => ($withoutMargin ? 0 : '26px')} 0;
|
||||
`
|
||||
|
||||
const StyledH4 = styled.h4<{ $withoutMargin?: boolean; $strong?: boolean }>`
|
||||
font-family: ${({ theme }) => theme.legacy?.fonts?.fontFamily || theme.typography?.fontFamily};
|
||||
font-size: ${({ theme }) => theme.legacy?.title?.size?.sm?.fontSize || '16px'};
|
||||
line-height: ${({ theme }) => theme.legacy?.title?.size?.sm?.lineHeight || '24px'};
|
||||
font-weight: ${({ $strong }) => ($strong ? 'bold' : 'normal')};
|
||||
margin: ${({ $withoutMargin }) => ($withoutMargin ? 0 : '22px')} 0;
|
||||
`
|
||||
|
||||
const StyledH5 = styled.h5<{ $withoutMargin?: boolean; $strong?: boolean }>`
|
||||
font-family: ${({ theme }) => theme.legacy?.fonts?.fontFamily || theme.typography?.fontFamily};
|
||||
font-size: ${({ theme }) => theme.legacy?.title?.size?.xs?.fontSize || '14px'};
|
||||
line-height: ${({ theme }) => theme.legacy?.title?.size?.xs?.lineHeight || '20px'};
|
||||
font-weight: ${({ $strong }) => ($strong ? 'bold' : 'normal')};
|
||||
margin: ${({ $withoutMargin }) => ($withoutMargin ? 0 : '18px')} 0;
|
||||
`
|
||||
|
||||
const Title = ({ children, size, withoutMargin, strong }: Props) => {
|
||||
const transientProps = { $withoutMargin: withoutMargin, $strong: strong }
|
||||
switch (size) {
|
||||
case 'xl': {
|
||||
return <StyledH1 {...transientProps}>{children}</StyledH1>
|
||||
}
|
||||
case 'lg': {
|
||||
return <StyledH2 {...transientProps}>{children}</StyledH2>
|
||||
}
|
||||
case 'md': {
|
||||
return <StyledH3 {...transientProps}>{children}</StyledH3>
|
||||
}
|
||||
case 'sm': {
|
||||
return <StyledH4 {...transientProps}>{children}</StyledH4>
|
||||
}
|
||||
case 'xs': {
|
||||
return <StyledH5 {...transientProps}>{children}</StyledH5>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Title
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ReactElement } from 'react'
|
||||
import MUITooltip, { TooltipProps as TooltipPropsMui } from '@mui/material/Tooltip'
|
||||
import { styled, alpha, type Theme } from '@mui/material/styles'
|
||||
import { PaletteColor } from '@mui/material/styles/createPalette'
|
||||
|
||||
type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
|
||||
type TooltipProps = {
|
||||
size?: SizeType
|
||||
backgroundColor?: keyof Theme['palette']
|
||||
textColor?: keyof Theme['palette']
|
||||
padding?: string
|
||||
border?: string
|
||||
}
|
||||
|
||||
const getPaddingBySize = (size: SizeType): string => {
|
||||
switch (size) {
|
||||
case 'lg':
|
||||
return '8px 16px'
|
||||
default:
|
||||
return '4px 8px'
|
||||
}
|
||||
}
|
||||
|
||||
const getBorderBySize = (size: SizeType): string => {
|
||||
switch (size) {
|
||||
case 'lg':
|
||||
return 'none'
|
||||
default:
|
||||
return `1px solid #B2B5B2`
|
||||
}
|
||||
}
|
||||
|
||||
const getFontInfoBySize = (
|
||||
size: SizeType,
|
||||
): {
|
||||
fontSize: string
|
||||
lineHeight: string
|
||||
} => {
|
||||
switch (size) {
|
||||
case 'lg':
|
||||
return {
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
}
|
||||
default:
|
||||
return {
|
||||
fontSize: '12px',
|
||||
lineHeight: '16px',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface StyledTooltipProps {
|
||||
backgroundColor?: keyof Theme['palette']
|
||||
textColor?: keyof Theme['palette']
|
||||
tooltipSize?: SizeType
|
||||
}
|
||||
|
||||
const StyledTooltip = styled(MUITooltip, {
|
||||
shouldForwardProp: (prop) => !['backgroundColor', 'textColor', 'tooltipSize'].includes(prop as string),
|
||||
})<StyledTooltipProps>(({ theme, backgroundColor, textColor, tooltipSize = 'md' }) => ({
|
||||
'& .MuiTooltip-popper': {
|
||||
zIndex: 2001,
|
||||
},
|
||||
'& .MuiTooltip-tooltip': {
|
||||
backgroundColor:
|
||||
backgroundColor && theme.palette[backgroundColor]
|
||||
? (theme.palette[backgroundColor] as PaletteColor).main
|
||||
: theme.palette.primary.main,
|
||||
boxShadow: `1px 2px 10px ${alpha('#28363D', 0.18)}`,
|
||||
border: getBorderBySize(tooltipSize),
|
||||
color: textColor ? (theme.palette[textColor] as PaletteColor).main : theme.palette.background.default,
|
||||
borderRadius: '4px',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
padding: getPaddingBySize(tooltipSize),
|
||||
fontSize: getFontInfoBySize(tooltipSize).fontSize,
|
||||
lineHeight: getFontInfoBySize(tooltipSize).lineHeight,
|
||||
},
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: backgroundColor ? (theme.palette[backgroundColor] as PaletteColor).main : '#E8E7E6',
|
||||
border: 'none',
|
||||
'&::before': {
|
||||
boxShadow: `1px 2px 10px ${alpha('#28363D', 0.18)}`,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
children: ReactElement
|
||||
} & TooltipProps
|
||||
|
||||
export const Tooltip = ({
|
||||
title,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
children,
|
||||
size,
|
||||
...rest
|
||||
}: Props & Omit<TooltipPropsMui, 'title'>): ReactElement => {
|
||||
return (
|
||||
<StyledTooltip title={title} backgroundColor={backgroundColor} textColor={textColor} tooltipSize={size} {...rest}>
|
||||
{children}
|
||||
</StyledTooltip>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import { AccordionDetails, IconButton } from '@mui/material'
|
||||
import { memo, useState } from 'react'
|
||||
import { DraggableProvided, DraggableStateSnapshot } from '@hello-pangea/dnd'
|
||||
import styled from 'styled-components'
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator'
|
||||
import { ProposedTransaction } from '../typings/models'
|
||||
import TransactionDetails from './TransactionDetails'
|
||||
import { getTransactionText } from '../utils'
|
||||
import Text from './Text'
|
||||
import { Accordion, AccordionSummary } from './Accordion'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import EthHashInfo from './ETHHashInfo'
|
||||
import { Icon } from './Icon'
|
||||
import FixedIcon from './FixedIcon'
|
||||
import Dot from './Dot'
|
||||
|
||||
const UNKNOWN_POSITION_LABEL = '?'
|
||||
const minArrowSize = '12'
|
||||
|
||||
type TransactionProps = {
|
||||
transaction: ProposedTransaction
|
||||
provided: DraggableProvided
|
||||
snapshot: DraggableStateSnapshot
|
||||
isLastTransaction: boolean
|
||||
showTransactionDetails: boolean
|
||||
index: number
|
||||
draggableTxIndexDestination: number | undefined
|
||||
draggableTxIndexOrigin: number | undefined
|
||||
reorderTransactions?: (sourceIndex: number, destinationIndex: number) => void
|
||||
networkPrefix: string | undefined
|
||||
replaceTransaction?: (newTransaction: ProposedTransaction, index: number) => void
|
||||
setTxIndexToEdit: (index: string) => void
|
||||
openEditTxModal: () => void
|
||||
removeTransaction?: (index: number) => void
|
||||
setTxIndexToRemove: (index: string) => void
|
||||
openDeleteTxModal: () => void
|
||||
}
|
||||
|
||||
const TransactionBatchListItem = memo(
|
||||
({
|
||||
transaction,
|
||||
provided,
|
||||
snapshot,
|
||||
isLastTransaction,
|
||||
showTransactionDetails,
|
||||
index,
|
||||
draggableTxIndexDestination,
|
||||
draggableTxIndexOrigin,
|
||||
reorderTransactions,
|
||||
networkPrefix,
|
||||
replaceTransaction,
|
||||
setTxIndexToEdit,
|
||||
openEditTxModal,
|
||||
removeTransaction,
|
||||
setTxIndexToRemove,
|
||||
openDeleteTxModal,
|
||||
}: TransactionProps) => {
|
||||
const { description } = transaction
|
||||
const { to } = description
|
||||
|
||||
const transactionDescription = getTransactionText(description)
|
||||
|
||||
const [isTxExpanded, setTxExpanded] = useState(false)
|
||||
|
||||
const onClickShowTransactionDetails = () => {
|
||||
if (showTransactionDetails) {
|
||||
setTxExpanded((isTxExpanded) => !isTxExpanded)
|
||||
}
|
||||
}
|
||||
const isThisTxBeingDragging = snapshot.isDragging
|
||||
|
||||
const showArrowAdornment = !isLastTransaction && !isThisTxBeingDragging
|
||||
|
||||
// displayed order can change if the user uses the drag and drop feature
|
||||
const displayedTxPosition = getDisplayedTxPosition(
|
||||
index,
|
||||
isThisTxBeingDragging,
|
||||
draggableTxIndexDestination,
|
||||
draggableTxIndexOrigin,
|
||||
)
|
||||
|
||||
return (
|
||||
<TransactionListItem ref={provided.innerRef} {...provided.draggableProps}>
|
||||
{/* Transacion Position */}
|
||||
<PositionWrapper>
|
||||
<PositionDot color="primary" isDragging={isThisTxBeingDragging}>
|
||||
<Text>{displayedTxPosition}</Text>
|
||||
</PositionDot>
|
||||
{showArrowAdornment && <ArrowAdornment />}
|
||||
</PositionWrapper>
|
||||
|
||||
{/* Transaction Description */}
|
||||
<StyledAccordion
|
||||
expanded={isTxExpanded}
|
||||
compact
|
||||
onChange={onClickShowTransactionDetails}
|
||||
isDragging={isThisTxBeingDragging}
|
||||
TransitionProps={{ unmountOnExit: true }}
|
||||
>
|
||||
<div {...provided.dragHandleProps}>
|
||||
<AccordionSummary expandIcon={false} style={{ cursor: reorderTransactions ? 'grab' : 'pointer' }}>
|
||||
{/* Drag & Drop Indicator */}
|
||||
{reorderTransactions && (
|
||||
<Tooltip placement="top" title="Drag and Drop" backgroundColor="primary" arrow>
|
||||
<DragAndDropIndicatorIcon fontSize="small" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Destination Address label */}
|
||||
<StyledEthHashInfo shortName={networkPrefix || ''} hash={to} shortenHash={4} shouldShowShortName />
|
||||
|
||||
{/* Transaction Description label */}
|
||||
<TransactionsDescription>{transactionDescription}</TransactionsDescription>
|
||||
|
||||
{/* Transaction Actions */}
|
||||
|
||||
{/* Edit transaction */}
|
||||
{replaceTransaction && (
|
||||
<Tooltip title="Edit transaction" backgroundColor="primary" arrow>
|
||||
<TransactionActionButton
|
||||
size="medium"
|
||||
aria-label="Edit transaction"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setTxIndexToEdit(String(index))
|
||||
openEditTxModal()
|
||||
}}
|
||||
>
|
||||
<Icon size="sm" type="edit" />
|
||||
</TransactionActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Delete transaction */}
|
||||
{removeTransaction && (
|
||||
<Tooltip placement="top" title="Delete transaction" backgroundColor="primary" arrow>
|
||||
<TransactionActionButton
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setTxIndexToRemove(String(index))
|
||||
openDeleteTxModal()
|
||||
}}
|
||||
size="medium"
|
||||
aria-label="Delete transaction"
|
||||
>
|
||||
<Icon size="sm" type="delete" />
|
||||
</TransactionActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Expand transaction details */}
|
||||
{showTransactionDetails && (
|
||||
<Tooltip placement="top" title="Expand transaction details" backgroundColor="primary" arrow>
|
||||
<TransactionActionButton
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onClickShowTransactionDetails()
|
||||
}}
|
||||
size="medium"
|
||||
aria-label="Expand transaction details"
|
||||
>
|
||||
<StyledArrow isTxExpanded={isTxExpanded} type={'chevronDown'} />
|
||||
</TransactionActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</AccordionSummary>
|
||||
</div>
|
||||
|
||||
{/* Transaction details */}
|
||||
<AccordionDetails>
|
||||
<TransactionDetails transaction={transaction} />
|
||||
</AccordionDetails>
|
||||
</StyledAccordion>
|
||||
</TransactionListItem>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const getDisplayedTxPosition = (
|
||||
index: number,
|
||||
isDraggingThisTx: boolean,
|
||||
draggableTxIndexDestination?: number,
|
||||
draggableTxIndexOrigin?: number,
|
||||
): string => {
|
||||
// we show the correct position in the transaction that is being dragged
|
||||
if (isDraggingThisTx) {
|
||||
const isAwayFromDroppableZone = draggableTxIndexDestination === undefined
|
||||
return isAwayFromDroppableZone ? UNKNOWN_POSITION_LABEL : String(draggableTxIndexDestination + 1)
|
||||
}
|
||||
|
||||
// if a transaction is being dragged, we show the correct position in previous transactions
|
||||
if (index < Number(draggableTxIndexOrigin)) {
|
||||
// depending on the current destination we show the correct position
|
||||
return index >= Number(draggableTxIndexDestination) ? `${index + 2}` : `${index + 1}`
|
||||
}
|
||||
|
||||
// if a transaction is being dragged, we show the correct position in next transactions
|
||||
if (index > Number(draggableTxIndexOrigin)) {
|
||||
// depending on the current destination we show the correct position
|
||||
return index > Number(draggableTxIndexDestination) ? `${index + 1}` : `${index}`
|
||||
}
|
||||
|
||||
// otherwise we show the natural position
|
||||
return `${index + 1}`
|
||||
}
|
||||
|
||||
const TransactionListItem = styled.li`
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
const StyledArrow = styled(FixedIcon)<{ isTxExpanded: boolean }>`
|
||||
.icon-color {
|
||||
fill: #b2b5b2;
|
||||
}
|
||||
${({ isTxExpanded }) =>
|
||||
isTxExpanded &&
|
||||
`
|
||||
transform: rotateZ(180deg);
|
||||
|
||||
`}
|
||||
`
|
||||
|
||||
const PositionWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 14px 10px 0 0;
|
||||
`
|
||||
|
||||
const PositionDot = styled(Dot).withConfig({
|
||||
shouldForwardProp: (prop, defaultValidatorFn) => defaultValidatorFn(prop),
|
||||
})<{ isDragging: boolean }>`
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
background-color: ${({ theme }) => theme.palette.border.light};
|
||||
transition: background-color 0.5s linear;
|
||||
`
|
||||
|
||||
const ArrowAdornment = styled.div`
|
||||
position: relative;
|
||||
border-left: 1px solid ${({ theme }) => theme.palette.border.light};
|
||||
flex-grow: 1;
|
||||
margin-top: 8px;
|
||||
|
||||
&&::before {
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
border-left: 1px solid ${({ theme }) => theme.palette.border.light};
|
||||
|
||||
height: ${minArrowSize}px;
|
||||
bottom: -${minArrowSize}px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
&&::after {
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
bottom: -${minArrowSize}px;
|
||||
left: -4px;
|
||||
|
||||
border-width: 0 1px 1px 0;
|
||||
border-style: solid;
|
||||
border-color: ${({ theme }) => theme.palette.border.light};
|
||||
padding: 3px;
|
||||
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
`
|
||||
|
||||
// transaction description styles
|
||||
|
||||
const StyledAccordion = styled(Accordion).withConfig({
|
||||
shouldForwardProp: (prop) => !['isDragging'].includes(prop),
|
||||
})<{ isDragging: boolean }>`
|
||||
flex-grow: 1;
|
||||
|
||||
&.MuiAccordion-root {
|
||||
margin-bottom: 0;
|
||||
border-width: 1px;
|
||||
border-color: ${({ isDragging, expanded, theme }) =>
|
||||
isDragging || expanded ? theme.palette.secondary.light : theme.palette.background.paper};
|
||||
transition: border-color 0.5s linear;
|
||||
|
||||
&:hover {
|
||||
border-color: ${({ theme }) => theme.palette.secondary.light};
|
||||
|
||||
.MuiAccordionSummary-root {
|
||||
background-color: ${({ theme }) => theme.palette.secondary.background};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.MuiAccordionSummary-root {
|
||||
height: 52px;
|
||||
padding: 0px 8px;
|
||||
background-color: ${({ isDragging, theme }) =>
|
||||
isDragging ? theme.palette.secondary.background : theme.palette.background.paper};
|
||||
|
||||
.MuiIconButton-root {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&.Mui-expanded {
|
||||
border-width: 1px;
|
||||
background-color: ${({ theme }) => theme.palette.secondary.background};
|
||||
border-color: ${({ isDragging, expanded, theme }) =>
|
||||
isDragging || expanded ? theme.palette.secondary.light : '#e8e7e6'};
|
||||
}
|
||||
}
|
||||
|
||||
.MuiAccordionSummary-content {
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionActionButton = styled(IconButton)`
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const TransactionsDescription = styled(Text)`
|
||||
&& {
|
||||
flex-grow: 1;
|
||||
padding-left: 24px;
|
||||
font-size: 14px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`
|
||||
|
||||
const DragAndDropIndicatorIcon = styled(DragIndicatorIcon)`
|
||||
color: #b2bbc0;
|
||||
margin-right: 4px;
|
||||
`
|
||||
|
||||
const StyledEthHashInfo = styled(EthHashInfo)`
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
export default TransactionBatchListItem
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import useElementHeight from '../hooks/useElementHeight/useElementHeight'
|
||||
import { ProposedTransaction } from '../typings/models'
|
||||
import { weiToEther } from '../utils'
|
||||
import EthHashInfo from './ETHHashInfo'
|
||||
import Text from './Text'
|
||||
import { Typography } from '@mui/material'
|
||||
import ButtonLink from './buttons/ButtonLink'
|
||||
|
||||
type TransactionDetailsProp = {
|
||||
transaction: ProposedTransaction
|
||||
}
|
||||
|
||||
const TransactionDetails = ({ transaction }: TransactionDetailsProp) => {
|
||||
const { description, raw } = transaction
|
||||
|
||||
const { to, value, data } = raw
|
||||
const { contractMethod, contractFieldsValues, customTransactionData, networkPrefix, nativeCurrencySymbol } =
|
||||
description
|
||||
|
||||
const isCustomHexDataTx = !!customTransactionData
|
||||
const isContractInteractionTx = !!contractMethod && !isCustomHexDataTx
|
||||
|
||||
const isTokenTransferTx = !isCustomHexDataTx && !isContractInteractionTx
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<StyledTxTitle>
|
||||
{isTokenTransferTx ? `Transfer ${weiToEther(value)} ${nativeCurrencySymbol} to:` : 'Interact with:'}
|
||||
</StyledTxTitle>
|
||||
|
||||
<StyledEthHashInfo shortName={networkPrefix || ''} hash={to} showAvatar showCopyBtn shouldShowShortName />
|
||||
|
||||
<TxSummaryContainer>
|
||||
{/* to address */}
|
||||
<StyledText color="grey">to (address)</StyledText>
|
||||
<StyledEthHashInfo shortName={networkPrefix || ''} hash={to} shortenHash={4} showCopyBtn shouldShowShortName />
|
||||
|
||||
{/* value */}
|
||||
<StyledText color="grey">value:</StyledText>
|
||||
<TxValueLabel>{`${weiToEther(value)} ${nativeCurrencySymbol}`}</TxValueLabel>
|
||||
|
||||
{/* data */}
|
||||
<StyledText color="grey">data:</StyledText>
|
||||
<TxValueLabel>{data}</TxValueLabel>
|
||||
|
||||
{isContractInteractionTx && (
|
||||
<>
|
||||
{/* method */}
|
||||
<StyledText color="grey">method:</StyledText>
|
||||
<StyledTxValueLabel>{contractMethod.name}</StyledTxValueLabel>
|
||||
|
||||
{/* method inputs */}
|
||||
{contractMethod.inputs.map(({ name, type }, index) => {
|
||||
const inputName = name || index
|
||||
const inputLabel = `${inputName} (${type})`
|
||||
const inputValue = contractFieldsValues?.[inputName]
|
||||
return (
|
||||
<React.Fragment key={`${inputLabel}-${index}`}>
|
||||
{/* input name */}
|
||||
<StyledMethodNameLabel color="grey" tooltip={inputLabel}>
|
||||
{inputLabel}
|
||||
</StyledMethodNameLabel>
|
||||
{/* input value */}
|
||||
<TxValueLabel>{inputValue}</TxValueLabel>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</TxSummaryContainer>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransactionDetails
|
||||
|
||||
const Wrapper = styled.article`
|
||||
flex-grow: 1;
|
||||
padding: 0 16px;
|
||||
user-select: text;
|
||||
`
|
||||
|
||||
const TxSummaryContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: minmax(100px, 2fr) minmax(100px, 5fr);
|
||||
gap: 4px;
|
||||
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
const StyledTxTitle = styled(Typography)`
|
||||
&& {
|
||||
font-size: 16px;
|
||||
margin: 8px 0;
|
||||
font-weight: bold;
|
||||
line-height: initial;
|
||||
}
|
||||
`
|
||||
const StyledText = styled(Text)`
|
||||
&& {
|
||||
color: ${({ theme }) => theme.palette.text.secondary};
|
||||
font-weight: 400;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMethodNameLabel = styled(Text)`
|
||||
padding-left: 4px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const LINE_HEIGHT = 22
|
||||
const MAX_HEIGHT = 2 * LINE_HEIGHT // 2 lines as max height
|
||||
|
||||
const TxValueLabel = ({ children }: { children: React.ReactNode }) => {
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
const [showEllipsis, setShowEllipsis] = useState(false)
|
||||
|
||||
const { height: containerHeight, elementRef } = useElementHeight<HTMLDivElement>()
|
||||
|
||||
// we show the Show more/less button if the height is more than 44px (the height of 2 lines)
|
||||
const showMoreButton = containerHeight && containerHeight > MAX_HEIGHT
|
||||
|
||||
// we show/hide ellipsis at the end of the second line if user clicks on "Show more"
|
||||
useEffect(() => {
|
||||
if (showMoreButton && !showMore) {
|
||||
setShowEllipsis(true)
|
||||
}
|
||||
}, [showMoreButton, showMore])
|
||||
|
||||
return (
|
||||
<div ref={elementRef}>
|
||||
{/* value */}
|
||||
<StyledTxValueLabel showMore={showMore} showEllipsis={showEllipsis}>
|
||||
{children}
|
||||
</StyledTxValueLabel>
|
||||
|
||||
{/* show more/less button */}
|
||||
{showMoreButton && (
|
||||
<StyledButtonLink color="primary" onClick={() => setShowMore((showMore) => !showMore)}>
|
||||
{showMore ? 'Show less' : 'Show more'}
|
||||
</StyledButtonLink>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledTxValueLabel = styled(Text).withConfig({
|
||||
shouldForwardProp: (prop) => !['showMore'].includes(prop) || !['showEllipsis'].includes(prop),
|
||||
})<{ showMore?: boolean; showEllipsis?: boolean }>`
|
||||
&& {
|
||||
max-height: ${({ showMore }) => (showMore ? '100%' : `${MAX_HEIGHT + 1}px`)};
|
||||
font-size: 14px;
|
||||
line-break: anywhere;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
${({ showEllipsis, showMore }) =>
|
||||
!showMore &&
|
||||
showEllipsis &&
|
||||
`@supports (-webkit-line-clamp: 2) {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}`}
|
||||
}
|
||||
`
|
||||
const StyledEthHashInfo = styled(EthHashInfo)`
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledButtonLink = styled(ButtonLink)`
|
||||
padding: 0;
|
||||
|
||||
&& > p {
|
||||
margin 0;
|
||||
}
|
||||
|
||||
`
|
||||
@@ -0,0 +1,394 @@
|
||||
import { isValidElement, useMemo, useState } from 'react'
|
||||
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
DroppableProvided,
|
||||
DragStart,
|
||||
DragUpdate,
|
||||
DropResult,
|
||||
Draggable,
|
||||
DraggableProvided,
|
||||
DraggableStateSnapshot,
|
||||
} from '@hello-pangea/dnd'
|
||||
import { ProposedTransaction } from '../typings/models'
|
||||
import useModal from '../hooks/useModal/useModal'
|
||||
import DeleteTransactionModal from './modals/DeleteTransactionModal'
|
||||
import DeleteBatchModal from './modals/DeleteBatchModal'
|
||||
import SaveBatchModal from './modals/SaveBatchModal'
|
||||
import EditTransactionModal from './modals/EditTransactionModal'
|
||||
import { useNetwork, useTransactionLibrary } from '../store'
|
||||
import Item from './TransactionBatchListItem'
|
||||
import VirtualizedList from './VirtualizedList'
|
||||
import { getTransactionText } from '../utils'
|
||||
import { EditableLabelProps } from './EditableLabel'
|
||||
import Text from './Text'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import { Icon } from './Icon'
|
||||
import { Typography } from '@mui/material'
|
||||
import Dot from './Dot'
|
||||
|
||||
type TransactionsBatchListProps = {
|
||||
transactions: ProposedTransaction[]
|
||||
showTransactionDetails: boolean
|
||||
showBatchHeader: boolean
|
||||
// batch title has multiple types because there are files passing it as a string
|
||||
// or 2 types of components:
|
||||
// 1: apps/tx-builder/src/pages/EditTransactionLibrary.tsx
|
||||
// 2: apps/tx-builder/src/pages/CreateTransactions.tsx
|
||||
batchTitle?: string | React.ReactElement<EditableLabelProps> | React.ReactElement<{ filename: string }>
|
||||
removeTransaction?: (index: number) => void
|
||||
saveBatch?: (name: string, transactions: ProposedTransaction[]) => void
|
||||
downloadBatch?: (name: string, transactions: ProposedTransaction[]) => void
|
||||
removeAllTransactions?: () => void
|
||||
replaceTransaction?: (newTransaction: ProposedTransaction, index: number) => void
|
||||
reorderTransactions?: (sourceIndex: number, destinationIndex: number) => void
|
||||
}
|
||||
|
||||
const TRANSACTION_LIST_DROPPABLE_ID = 'Transaction_List'
|
||||
const DROP_EVENT = 'DROP'
|
||||
|
||||
const TransactionsBatchList = ({
|
||||
transactions,
|
||||
reorderTransactions,
|
||||
removeTransaction,
|
||||
removeAllTransactions,
|
||||
replaceTransaction,
|
||||
saveBatch,
|
||||
downloadBatch,
|
||||
showTransactionDetails,
|
||||
showBatchHeader,
|
||||
batchTitle,
|
||||
}: TransactionsBatchListProps) => {
|
||||
// we need those states to display the correct position in each tx during the drag & drop
|
||||
const { batch } = useTransactionLibrary()
|
||||
const [draggableTxIndexOrigin, setDraggableTxIndexOrigin] = useState<number>()
|
||||
const [draggableTxIndexDestination, setDraggableTxIndexDestination] = useState<number>()
|
||||
|
||||
const { networkPrefix, getAddressFromDomain, nativeCurrencySymbol } = useNetwork()
|
||||
|
||||
const onDragStart = ({ source }: DragStart) => {
|
||||
setDraggableTxIndexOrigin(source.index)
|
||||
setDraggableTxIndexDestination(source.index)
|
||||
}
|
||||
|
||||
const onDragUpdate = ({ source, destination }: DragUpdate) => {
|
||||
setDraggableTxIndexOrigin(source.index)
|
||||
setDraggableTxIndexDestination(destination?.index)
|
||||
}
|
||||
|
||||
// we only perform the reorder if its present
|
||||
const onDragEnd = ({ reason, source, destination }: DropResult) => {
|
||||
const sourceIndex = source.index
|
||||
const destinationIndex = destination?.index
|
||||
|
||||
const isDropEvent = reason === DROP_EVENT // because user can cancel the drag & drop
|
||||
const hasTxPositionChanged = sourceIndex !== destinationIndex && destinationIndex !== undefined
|
||||
|
||||
const shouldPerformTxReorder = isDropEvent && hasTxPositionChanged
|
||||
|
||||
if (shouldPerformTxReorder) {
|
||||
reorderTransactions?.(sourceIndex, destinationIndex)
|
||||
}
|
||||
|
||||
setDraggableTxIndexOrigin(undefined)
|
||||
setDraggableTxIndexDestination(undefined)
|
||||
}
|
||||
|
||||
// 5 modals needed: save batch modal, edit transaction modal, delete batch modal, delete transaction modal, download batch modal
|
||||
const { open: showDeleteBatchModal, openModal: openClearTransactions, closeModal: closeDeleteBatchModal } = useModal()
|
||||
const { open: showSaveBatchModal, openModal: openSaveBatchModal, closeModal: closeSaveBatchModal } = useModal()
|
||||
const { open: showDeleteTxModal, openModal: openDeleteTxModal, closeModal: closeDeleteTxModal } = useModal()
|
||||
const { open: showEditTxModal, openModal: openEditTxModal, closeModal: closeEditTxModal } = useModal()
|
||||
|
||||
const [txIndexToRemove, setTxIndexToRemove] = useState<string>()
|
||||
const [txIndexToEdit, setTxIndexToEdit] = useState<string>()
|
||||
|
||||
const fileName = useMemo(() => {
|
||||
if (isValidElement(batchTitle)) {
|
||||
if ('filename' in batchTitle.props) {
|
||||
return batchTitle.props.filename
|
||||
} else if (batchTitle.props.children) {
|
||||
return batchTitle.props.children.toString()
|
||||
}
|
||||
|
||||
return 'Untitled'
|
||||
}
|
||||
|
||||
return batchTitle || 'Untitled'
|
||||
}, [batchTitle])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TransactionsBatchWrapper>
|
||||
{/* Transactions Batch Header */}
|
||||
{showBatchHeader && (
|
||||
<TransactionHeader>
|
||||
{/* Transactions Batch Counter */}
|
||||
<TransactionCounterDot color="primary">
|
||||
<Text>{transactions.length}</Text>
|
||||
</TransactionCounterDot>
|
||||
|
||||
{/* Transactions Batch Title */}
|
||||
{batchTitle && <TransactionsTitle>{batchTitle}</TransactionsTitle>}
|
||||
|
||||
{/* Transactions Batch Actions */}
|
||||
{saveBatch && (
|
||||
<Tooltip placement="top" title="Save to Library" backgroundColor="primary" arrow>
|
||||
<StyledHeaderIconButton onClick={openSaveBatchModal}>
|
||||
<Icon
|
||||
size="sm"
|
||||
type={batch ? 'bookmarkFilled' : 'bookmark'}
|
||||
color="primary"
|
||||
aria-label="Save to Library"
|
||||
/>
|
||||
</StyledHeaderIconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{downloadBatch && (
|
||||
<Tooltip placement="top" title="Download" backgroundColor="primary" arrow>
|
||||
<StyledHeaderIconButton onClick={() => downloadBatch(fileName, transactions)}>
|
||||
<Icon size="sm" type="importImg" color="primary" aria-label="Download" />
|
||||
</StyledHeaderIconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{removeAllTransactions && (
|
||||
<Tooltip placement="top" title="Clear transactions" backgroundColor="primary" arrow>
|
||||
<StyledHeaderIconButton onClick={openClearTransactions}>
|
||||
<Icon size="sm" type="delete" color="error" aria-label="Clear transactions" />
|
||||
</StyledHeaderIconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TransactionHeader>
|
||||
)}
|
||||
|
||||
{/* Standard Transactions List */}
|
||||
{transactions.length <= 20 && (
|
||||
<DragDropContext onDragStart={onDragStart} onDragUpdate={onDragUpdate} onDragEnd={onDragEnd}>
|
||||
<Droppable mode={'standard'} droppableId={TRANSACTION_LIST_DROPPABLE_ID}>
|
||||
{(provided: DroppableProvided) => (
|
||||
<TransactionList {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{transactions.map((transaction: ProposedTransaction, index: number) => (
|
||||
<Draggable
|
||||
key={transaction.id}
|
||||
index={index}
|
||||
draggableId={transaction.id.toString()}
|
||||
isDragDisabled={!reorderTransactions}
|
||||
>
|
||||
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
|
||||
<Item
|
||||
key={transaction.id}
|
||||
transaction={transaction}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
isLastTransaction={index === transactions.length - 1}
|
||||
showTransactionDetails={showTransactionDetails}
|
||||
index={index}
|
||||
draggableTxIndexDestination={draggableTxIndexDestination}
|
||||
draggableTxIndexOrigin={draggableTxIndexOrigin}
|
||||
reorderTransactions={reorderTransactions}
|
||||
networkPrefix={networkPrefix}
|
||||
replaceTransaction={replaceTransaction}
|
||||
setTxIndexToEdit={setTxIndexToEdit}
|
||||
openEditTxModal={openEditTxModal}
|
||||
removeTransaction={removeTransaction}
|
||||
setTxIndexToRemove={setTxIndexToRemove}
|
||||
openDeleteTxModal={openDeleteTxModal}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</TransactionList>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)}
|
||||
|
||||
{/* Virtualized Transaction List */}
|
||||
{transactions.length > 20 && (
|
||||
<DragDropContext onDragStart={onDragStart} onDragUpdate={onDragUpdate} onDragEnd={onDragEnd}>
|
||||
<Droppable
|
||||
mode={'virtual'}
|
||||
droppableId={TRANSACTION_LIST_DROPPABLE_ID}
|
||||
renderClone={(provided: DraggableProvided, snapshot: DraggableStateSnapshot, rubric) => (
|
||||
<Item
|
||||
key={transactions[rubric.source.index].id}
|
||||
transaction={transactions[rubric.source.index]}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
isLastTransaction={rubric.source.index === transactions.length - 1}
|
||||
showTransactionDetails={showTransactionDetails}
|
||||
index={rubric.source.index}
|
||||
draggableTxIndexDestination={draggableTxIndexDestination}
|
||||
draggableTxIndexOrigin={draggableTxIndexOrigin}
|
||||
reorderTransactions={reorderTransactions}
|
||||
networkPrefix={networkPrefix}
|
||||
replaceTransaction={replaceTransaction}
|
||||
setTxIndexToEdit={setTxIndexToEdit}
|
||||
openEditTxModal={openEditTxModal}
|
||||
removeTransaction={removeTransaction}
|
||||
setTxIndexToRemove={setTxIndexToRemove}
|
||||
openDeleteTxModal={openDeleteTxModal}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(provided: DroppableProvided) => (
|
||||
<TransactionList {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<VirtualizedList
|
||||
innerRef={provided.innerRef}
|
||||
items={transactions}
|
||||
renderItem={(transaction: ProposedTransaction, index: number) => (
|
||||
<Draggable
|
||||
key={transaction.id}
|
||||
index={index}
|
||||
draggableId={transaction.id.toString()}
|
||||
isDragDisabled={!reorderTransactions}
|
||||
>
|
||||
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
|
||||
<Item
|
||||
key={transaction.id}
|
||||
transaction={transaction}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
isLastTransaction={index === transactions.length - 1}
|
||||
showTransactionDetails={showTransactionDetails}
|
||||
index={index}
|
||||
draggableTxIndexDestination={draggableTxIndexDestination}
|
||||
draggableTxIndexOrigin={draggableTxIndexOrigin}
|
||||
reorderTransactions={reorderTransactions}
|
||||
networkPrefix={networkPrefix}
|
||||
replaceTransaction={replaceTransaction}
|
||||
setTxIndexToEdit={setTxIndexToEdit}
|
||||
openEditTxModal={openEditTxModal}
|
||||
removeTransaction={removeTransaction}
|
||||
setTxIndexToRemove={setTxIndexToRemove}
|
||||
openDeleteTxModal={openDeleteTxModal}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
)}
|
||||
/>
|
||||
{transactions.length <= 20 && provided.placeholder}
|
||||
</TransactionList>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)}
|
||||
</TransactionsBatchWrapper>
|
||||
|
||||
{/* Edit transaction modal */}
|
||||
{showEditTxModal && (
|
||||
<EditTransactionModal
|
||||
txIndex={Number(txIndexToEdit)}
|
||||
transaction={transactions[Number(txIndexToEdit)]}
|
||||
onSubmit={(updatedTransaction: ProposedTransaction) => {
|
||||
closeEditTxModal()
|
||||
replaceTransaction?.(updatedTransaction, Number(txIndexToEdit))
|
||||
}}
|
||||
onDeleteTx={() => {
|
||||
closeEditTxModal()
|
||||
removeTransaction?.(Number(txIndexToEdit))
|
||||
}}
|
||||
onClose={closeEditTxModal}
|
||||
networkPrefix={networkPrefix}
|
||||
getAddressFromDomain={getAddressFromDomain}
|
||||
nativeCurrencySymbol={nativeCurrencySymbol}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete batch modal */}
|
||||
{showDeleteBatchModal && removeAllTransactions && (
|
||||
<DeleteBatchModal
|
||||
count={transactions.length}
|
||||
onClick={() => {
|
||||
closeDeleteBatchModal()
|
||||
removeAllTransactions()
|
||||
}}
|
||||
onClose={closeDeleteBatchModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete a transaction modal */}
|
||||
{showDeleteTxModal && (
|
||||
<DeleteTransactionModal
|
||||
txIndex={Number(txIndexToRemove)}
|
||||
txDescription={getTransactionText(transactions[Number(txIndexToRemove)]?.description)}
|
||||
onClick={() => {
|
||||
closeDeleteTxModal()
|
||||
removeTransaction?.(Number(txIndexToRemove))
|
||||
}}
|
||||
onClose={closeDeleteTxModal}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Save batch modal */}
|
||||
{showSaveBatchModal && (
|
||||
<SaveBatchModal
|
||||
onClick={(name: string) => {
|
||||
closeSaveBatchModal()
|
||||
saveBatch?.(name, transactions)
|
||||
}}
|
||||
onClose={closeSaveBatchModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransactionsBatchList
|
||||
|
||||
// tx positions can change during drag & drop
|
||||
|
||||
const TransactionsBatchWrapper = styled.section`
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
// batch header styles
|
||||
|
||||
const TransactionHeader = styled.header`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const TransactionCounterDot = styled(Dot)`
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
|
||||
p {
|
||||
color: ${({ theme }) => theme.palette.background.main};
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionsTitle = styled(Typography)`
|
||||
&& {
|
||||
flex-grow: 1;
|
||||
margin-left: 14px;
|
||||
min-width: 0;
|
||||
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledHeaderIconButton = styled(IconButton)`
|
||||
&.MuiIconButton-root {
|
||||
border-radius: 4px;
|
||||
background-color: ${({ theme }) => theme.palette.code.main};
|
||||
margin-left: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
// transactions list styles
|
||||
|
||||
const TransactionList = styled.ol`
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
`
|
||||
@@ -0,0 +1,64 @@
|
||||
import { memo, useEffect, useState, ReactNode } from 'react'
|
||||
import { Virtuoso } from 'react-virtuoso'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type VirtualizedListProps<T> = {
|
||||
innerRef: (ref: HTMLElement | null) => void
|
||||
items: T[]
|
||||
renderItem: (item: T, index: number) => ReactNode
|
||||
}
|
||||
|
||||
const VirtualizedList = <T,>({ innerRef, items, renderItem }: VirtualizedListProps<T>) => {
|
||||
const handleScrollerRef = (ref: HTMLElement | Window | null) => {
|
||||
if (ref instanceof HTMLElement || ref === null) {
|
||||
innerRef(ref)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
style={{ height: 600 }}
|
||||
scrollerRef={handleScrollerRef}
|
||||
data={items}
|
||||
itemContent={(index, item) => renderItem(item, index)}
|
||||
components={{
|
||||
Item: HeightPreservingItem,
|
||||
}}
|
||||
totalCount={items.length}
|
||||
overscan={100}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface HeightPreservingItemProps {
|
||||
children?: ReactNode
|
||||
'data-known-size'?: number
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const HeightPreservingItem = memo(({ children, ...props }: HeightPreservingItemProps) => {
|
||||
const [size, setSize] = useState(0)
|
||||
const knownSize = props['data-known-size']
|
||||
|
||||
useEffect(() => {
|
||||
setSize((prevSize) => {
|
||||
return knownSize === 0 ? prevSize : (knownSize ?? 0)
|
||||
})
|
||||
}, [knownSize])
|
||||
|
||||
return (
|
||||
<HeightPreservingContainer {...props} size={size}>
|
||||
{children}
|
||||
</HeightPreservingContainer>
|
||||
)
|
||||
})
|
||||
|
||||
const HeightPreservingContainer = styled.div<{ size: number }>`
|
||||
--child-height: ${(props) => `${props.size}px`};
|
||||
&:empty {
|
||||
min-height: calc(var(--child-height));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`
|
||||
|
||||
export default VirtualizedList
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
function Wrapper({ children, centered }: { children: React.ReactNode; centered?: boolean }) {
|
||||
return (
|
||||
<StyledWrapper $centered={centered}>
|
||||
<section>{children}</section>
|
||||
</StyledWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledWrapper = styled.main<{ $centered?: boolean }>`
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
background: ${({ theme }) => theme.palette.background.main};
|
||||
color: ${({ theme }) => theme.palette.text.primary};
|
||||
|
||||
> section {
|
||||
width: 100%;
|
||||
padding: 120px 4rem 48px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
max-width: ${({ $centered }) => ($centered ? '1000px' : '1500px')};
|
||||
}
|
||||
`
|
||||
|
||||
export default Wrapper
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Icon, IconProps, IconType } from '../../Icon'
|
||||
import Text from '../../Text'
|
||||
import { TypographyProps } from '@mui/material'
|
||||
import { type Theme } from '@mui/material/styles'
|
||||
|
||||
export interface Props extends React.ComponentPropsWithoutRef<'button'> {
|
||||
iconType?: keyof IconType
|
||||
iconSize?: IconProps['size']
|
||||
textSize?: TypographyProps['variant']
|
||||
color: keyof Theme['palette']
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const StyledButtonLink = styled.button<Props>`
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: ${({ theme, color }) => theme.palette[color].main};
|
||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledText = styled(Text)`
|
||||
margin: 0 4px;
|
||||
`
|
||||
|
||||
const ButtonLink = ({
|
||||
iconType,
|
||||
iconSize = 'md',
|
||||
children,
|
||||
textSize = 'body1',
|
||||
...rest
|
||||
}: Props): React.ReactElement => {
|
||||
return (
|
||||
<StyledButtonLink {...rest}>
|
||||
{iconType && <Icon size={iconSize} color={rest.color} type={iconType} />}
|
||||
<StyledText variant={textSize} color={rest.color}>
|
||||
{children}
|
||||
</StyledText>
|
||||
</StyledButtonLink>
|
||||
)
|
||||
}
|
||||
|
||||
export default ButtonLink
|
||||
@@ -0,0 +1,24 @@
|
||||
const copyTextToClipboard = (text: string): void => {
|
||||
const listener = (e: ClipboardEvent): void => {
|
||||
e.preventDefault()
|
||||
if (e.clipboardData) {
|
||||
e.clipboardData.setData('text/plain', text)
|
||||
}
|
||||
}
|
||||
|
||||
const range = document.createRange()
|
||||
|
||||
const documentSelection = document.getSelection()
|
||||
if (!documentSelection) {
|
||||
return
|
||||
}
|
||||
|
||||
range.selectNodeContents(document.body)
|
||||
documentSelection.addRange(range)
|
||||
document.addEventListener('copy', listener)
|
||||
document.execCommand('copy')
|
||||
document.removeEventListener('copy', listener)
|
||||
documentSelection.removeAllRanges()
|
||||
}
|
||||
|
||||
export default copyTextToClipboard
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import copyTextToClipboard from './copyTextToClipboard'
|
||||
import { Icon } from '../../Icon'
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
outline-color: transparent;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.palette.divider};
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
textToCopy: string
|
||||
className?: string
|
||||
iconType?: Parameters<typeof Icon>[0]['type']
|
||||
tooltip?: string
|
||||
tooltipAfterCopy?: string
|
||||
}
|
||||
|
||||
const CopyToClipboardBtn = ({
|
||||
className,
|
||||
textToCopy,
|
||||
iconType = 'copy',
|
||||
tooltip = 'Copy to clipboard',
|
||||
}: Props): React.ReactElement => {
|
||||
const [clicked, setClicked] = useState<boolean>(false)
|
||||
|
||||
const copy = () => {
|
||||
copyTextToClipboard(textToCopy)
|
||||
setClicked(true)
|
||||
}
|
||||
|
||||
const onButtonClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
event.stopPropagation()
|
||||
copy()
|
||||
}
|
||||
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
|
||||
// prevents event from bubbling when `Enter` is pressed
|
||||
if (event.keyCode === 13) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
copy()
|
||||
}
|
||||
|
||||
const onButtonBlur = (): void => {
|
||||
setTimeout((): void => setClicked(false), 300)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
className={className}
|
||||
type="button"
|
||||
onClick={onButtonClick}
|
||||
onKeyDown={onKeyDown}
|
||||
onMouseLeave={onButtonBlur}
|
||||
>
|
||||
<Icon size="sm" type={iconType} tooltip={clicked ? 'Copied' : tooltip} />
|
||||
</StyledButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyToClipboardBtn
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Icon } from '../../Icon'
|
||||
import { ExplorerInfo } from '../../ETHHashInfo'
|
||||
|
||||
const StyledLink = styled.a`
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
outline-color: transparent;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
:hover {
|
||||
background-color: #f0efee;
|
||||
}
|
||||
`
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
explorerUrl: ExplorerInfo
|
||||
}
|
||||
|
||||
const ExplorerButton = ({ className, explorerUrl }: Props): React.ReactElement => {
|
||||
const { url, alt } = explorerUrl()
|
||||
const onClick = (event: React.MouseEvent<HTMLAnchorElement>): void => {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLAnchorElement>): void => {
|
||||
// prevents event from bubbling when `Enter` is pressed
|
||||
if (event.keyCode === 13) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
className={className}
|
||||
aria-label="Show details on Etherscan"
|
||||
rel="noopener noreferrer"
|
||||
onClick={onClick}
|
||||
href={url}
|
||||
target="_blank"
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<Icon size="sm" type="externalLink" tooltip={alt} />
|
||||
</StyledLink>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExplorerButton
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import makeBlockie from 'ethereum-blockies-base64'
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const identiconSizes = {
|
||||
xs: '10px',
|
||||
sm: '16px',
|
||||
md: '32px',
|
||||
lg: '40px',
|
||||
xl: '48px',
|
||||
xxl: '60px',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
address: string
|
||||
size: keyof typeof identiconSizes
|
||||
}
|
||||
|
||||
const StyledImg = styled.img<{ size: keyof typeof identiconSizes }>`
|
||||
height: ${({ size }) => identiconSizes[size]};
|
||||
width: ${({ size }) => identiconSizes[size]};
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
const Identicon = ({ size = 'md', address, ...rest }: Props): React.ReactElement => {
|
||||
const iconSrc = React.useMemo(() => makeBlockie(address), [address])
|
||||
|
||||
return <StyledImg src={iconSrc} size={size} {...rest} />
|
||||
}
|
||||
|
||||
export default Identicon
|
||||
@@ -0,0 +1,82 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ContractInterface } from '../../typings/models'
|
||||
import { isValidAddress } from '../../utils'
|
||||
import SolidityForm, {
|
||||
CONTRACT_METHOD_INDEX_FIELD_NAME,
|
||||
SolidityFormValuesTypes,
|
||||
TO_ADDRESS_FIELD_NAME,
|
||||
parseFormToProposedTransaction,
|
||||
} from './SolidityForm'
|
||||
import { useTransactions, useNetwork } from '../../store'
|
||||
import { Typography } from '@mui/material'
|
||||
import Button from '../Button'
|
||||
import FixedIcon from '../FixedIcon'
|
||||
|
||||
type AddNewTransactionFormProps = {
|
||||
contract: ContractInterface | null
|
||||
to: string
|
||||
showHexEncodedData: boolean
|
||||
}
|
||||
|
||||
const AddNewTransactionForm = ({ contract, to, showHexEncodedData }: AddNewTransactionFormProps) => {
|
||||
const initialFormValues = {
|
||||
[TO_ADDRESS_FIELD_NAME]: isValidAddress(to) ? to : '',
|
||||
[CONTRACT_METHOD_INDEX_FIELD_NAME]: '0',
|
||||
}
|
||||
|
||||
const { addTransaction } = useTransactions()
|
||||
const { networkPrefix, getAddressFromDomain, nativeCurrencySymbol } = useNetwork()
|
||||
|
||||
const onSubmit = (values: SolidityFormValuesTypes) => {
|
||||
const proposedTransaction = parseFormToProposedTransaction(values, contract, nativeCurrencySymbol, networkPrefix)
|
||||
|
||||
addTransaction(proposedTransaction)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body1" paragraph>
|
||||
Transaction information
|
||||
</Typography>
|
||||
|
||||
<SolidityForm
|
||||
id="solidity-contract-form"
|
||||
initialValues={initialFormValues}
|
||||
contract={contract}
|
||||
getAddressFromDomain={getAddressFromDomain}
|
||||
nativeCurrencySymbol={nativeCurrencySymbol}
|
||||
networkPrefix={networkPrefix}
|
||||
onSubmit={onSubmit}
|
||||
showHexEncodedData={showHexEncodedData}
|
||||
>
|
||||
<ButtonContainer>
|
||||
{/* Add transaction btn */}
|
||||
<Button variant="contained" color="primary" type="submit">
|
||||
<FixedIcon type={'plus'} />
|
||||
<StyledButtonLabel>Add new transaction</StyledButtonLabel>
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</SolidityForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddNewTransactionForm
|
||||
|
||||
const StyledButtonLabel = styled.span`
|
||||
margin-left: 8px;
|
||||
`
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
|
||||
.MuiButton-root {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,165 @@
|
||||
import { screen, waitFor, act, getByText, fireEvent } from '@testing-library/react'
|
||||
|
||||
import { render } from '../../test-utils'
|
||||
import { ContractInterface } from '../../typings/models'
|
||||
import SolidityForm, { CONTRACT_METHOD_INDEX_FIELD_NAME, TO_ADDRESS_FIELD_NAME } from './SolidityForm'
|
||||
|
||||
// Axios is bundled as ESM module which is not directly compatible with Jest
|
||||
// https://jestjs.io/docs/ecmascript-modules
|
||||
jest.mock('axios', () => ({
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
}))
|
||||
|
||||
const testAddressMethod = {
|
||||
inputs: [{ internalType: 'address', name: 'newValue', type: 'address' }],
|
||||
name: 'testAddressValue',
|
||||
payable: false,
|
||||
}
|
||||
|
||||
const testBooleanMethod = {
|
||||
inputs: [{ internalType: 'bool', name: 'newValue', type: 'bool' }],
|
||||
name: 'testBooleanValue',
|
||||
payable: false,
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
[TO_ADDRESS_FIELD_NAME]: '0x680cde08860141F9D223cE4E620B10Cd6741037E',
|
||||
[CONTRACT_METHOD_INDEX_FIELD_NAME]: '0',
|
||||
}
|
||||
|
||||
const testContract: ContractInterface = {
|
||||
methods: [testAddressMethod, testBooleanMethod],
|
||||
}
|
||||
|
||||
describe('<SolidityForm>', () => {
|
||||
it('Renders SolidityForm component', async () => {
|
||||
render(
|
||||
<SolidityForm
|
||||
id={'test-form'}
|
||||
onSubmit={jest.fn()}
|
||||
getAddressFromDomain={jest.fn()}
|
||||
initialValues={initialValues}
|
||||
contract={testContract}
|
||||
nativeCurrencySymbol={'ETH'}
|
||||
networkPrefix={'rin:'}
|
||||
showHexEncodedData={false}
|
||||
>
|
||||
<button type="submit">submit</button>
|
||||
</SolidityForm>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test-form')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('Show correct field contract params', async () => {
|
||||
render(
|
||||
<SolidityForm
|
||||
id={'test-form'}
|
||||
onSubmit={jest.fn()}
|
||||
getAddressFromDomain={jest.fn()}
|
||||
initialValues={initialValues}
|
||||
contract={testContract}
|
||||
nativeCurrencySymbol={'ETH'}
|
||||
networkPrefix={'rin:'}
|
||||
showHexEncodedData={false}
|
||||
>
|
||||
<button type="submit">submit</button>
|
||||
</SolidityForm>,
|
||||
)
|
||||
|
||||
let input: ReturnType<typeof screen.getByRole>
|
||||
|
||||
// testAddressMethod is selected by default
|
||||
await waitFor(() => {
|
||||
input = screen.getByRole('combobox')
|
||||
expect(input).toHaveValue('testAddressValue')
|
||||
})
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value: 'testBooleanVa' } })
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
})
|
||||
|
||||
// now testBooleanMethod is selected by default
|
||||
await waitFor(() => {
|
||||
// expect(input).toHaveValue('testBooleanValue')
|
||||
})
|
||||
})
|
||||
|
||||
// see https://github.com/safe-global/safe-react-apps/issues/450
|
||||
xit('Avoid collisions between parameters with the same name and different types when changing contract methods', async () => {
|
||||
render(
|
||||
<SolidityForm
|
||||
id={'test-form'}
|
||||
onSubmit={jest.fn()}
|
||||
getAddressFromDomain={jest.fn()}
|
||||
initialValues={initialValues}
|
||||
contract={testContract}
|
||||
nativeCurrencySymbol={'ETH'}
|
||||
networkPrefix={'rin:'}
|
||||
showHexEncodedData={false}
|
||||
>
|
||||
<button type="submit">submit</button>
|
||||
</SolidityForm>,
|
||||
)
|
||||
|
||||
// testAddressMethod is selected by default
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('testAddressValue')).toBeInTheDocument()
|
||||
expect(screen.queryByText('testBooleanValue')).not.toBeInTheDocument()
|
||||
|
||||
// no value by default
|
||||
expect(screen.getByTestId('contract-field-newValue')).toHaveValue('')
|
||||
})
|
||||
|
||||
// we update the address field value
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.getByTestId('contract-field-newValue'), {
|
||||
target: { value: '0x680cde08860141F9D223cE4E620B10Cd6741037E' },
|
||||
})
|
||||
})
|
||||
|
||||
// we update the address field value
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('contract-field-newValue')).toHaveValue('0x680cde08860141F9D223cE4E620B10Cd6741037E')
|
||||
})
|
||||
|
||||
// selects a different contract method
|
||||
await waitFor(() => {
|
||||
const contractMethodSelectorNode = screen.getByTestId('contract-method-selector')
|
||||
|
||||
// opens the contract method selector
|
||||
fireEvent.mouseDown(contractMethodSelectorNode)
|
||||
|
||||
// we select the boolean contract method
|
||||
const selectorModal = screen.getByTestId('menu-contractMethodIndex')
|
||||
fireEvent.click(getByText(selectorModal, 'testBooleanValue'))
|
||||
})
|
||||
|
||||
// the issue is not present (true value as default for booleans)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('contract-field-newValue-input')).toHaveValue('true')
|
||||
})
|
||||
|
||||
// address value again if we select testAddressMethod again
|
||||
await waitFor(() => {
|
||||
const contractMethodSelectorNode = screen.getByTestId('contract-method-selector')
|
||||
|
||||
// opens the contract method selector
|
||||
fireEvent.mouseDown(contractMethodSelectorNode)
|
||||
|
||||
// we select the boolean contract method
|
||||
const selectorModal = screen.getByTestId('menu-contractMethodIndex')
|
||||
fireEvent.click(getByText(selectorModal, 'testAddressValue'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('contract-field-newValue')).toHaveValue('0x680cde08860141F9D223cE4E620B10Cd6741037E')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,251 @@
|
||||
import { useEffect } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { DevTool } from '@hookform/devtools'
|
||||
import { getAddress, parseEther } from 'ethers'
|
||||
|
||||
import {
|
||||
ADDRESS_FIELD_TYPE,
|
||||
CONTRACT_METHOD_FIELD_TYPE,
|
||||
CUSTOM_TRANSACTION_DATA_FIELD_TYPE,
|
||||
NATIVE_AMOUNT_FIELD_TYPE,
|
||||
} from './fields/fields'
|
||||
import Field from './fields/Field'
|
||||
import { encodeToHexData, getInputTypeHelper } from '../../utils'
|
||||
import { ContractInterface, ProposedTransaction } from '../../typings/models'
|
||||
import { isProdEnv } from '../../utils/env'
|
||||
|
||||
export const TO_ADDRESS_FIELD_NAME = 'toAddress'
|
||||
export const NATIVE_VALUE_FIELD_NAME = 'nativeAmount'
|
||||
export const CONTRACT_METHOD_INDEX_FIELD_NAME = 'contractMethodIndex'
|
||||
export const CONTRACT_VALUES_FIELD_NAME = 'contractFieldsValues'
|
||||
export const CUSTOM_TRANSACTION_DATA_FIELD_NAME = 'customTransactionData'
|
||||
|
||||
type SolidityFormPropsTypes = {
|
||||
id: string
|
||||
networkPrefix: undefined | string
|
||||
getAddressFromDomain: (name: string) => Promise<string>
|
||||
nativeCurrencySymbol: undefined | string
|
||||
contract: ContractInterface | null
|
||||
onSubmit: SubmitHandler<SolidityFormValuesTypes>
|
||||
initialValues?: Partial<SolidityInitialFormValuesTypes>
|
||||
showHexToggler?: boolean
|
||||
children: React.ReactNode
|
||||
showHexEncodedData: boolean
|
||||
}
|
||||
|
||||
export type SolidityInitialFormValuesTypes = {
|
||||
[TO_ADDRESS_FIELD_NAME]: string
|
||||
[CONTRACT_METHOD_INDEX_FIELD_NAME]: string
|
||||
}
|
||||
|
||||
export type SolidityFormValuesTypes = {
|
||||
[TO_ADDRESS_FIELD_NAME]: string
|
||||
[NATIVE_VALUE_FIELD_NAME]: string
|
||||
[CONTRACT_METHOD_INDEX_FIELD_NAME]: string
|
||||
[CONTRACT_VALUES_FIELD_NAME]: Record<string, Record<string, string>>
|
||||
[CUSTOM_TRANSACTION_DATA_FIELD_NAME]: string
|
||||
}
|
||||
|
||||
export const parseFormToProposedTransaction = (
|
||||
values: SolidityFormValuesTypes,
|
||||
contract: ContractInterface | null,
|
||||
nativeCurrencySymbol: string | undefined,
|
||||
networkPrefix: string | undefined,
|
||||
): ProposedTransaction => {
|
||||
const contractMethodIndex = values[CONTRACT_METHOD_INDEX_FIELD_NAME]
|
||||
const toAddress = values[TO_ADDRESS_FIELD_NAME]
|
||||
const tokenValue = values[NATIVE_VALUE_FIELD_NAME]
|
||||
const contractFieldsValues = values[CONTRACT_VALUES_FIELD_NAME]
|
||||
const methodValues = contractFieldsValues?.[`method-${contractMethodIndex}`]
|
||||
const customTransactionData = values[CUSTOM_TRANSACTION_DATA_FIELD_NAME]
|
||||
|
||||
const contractMethod = contract?.methods[Number(contractMethodIndex)]
|
||||
|
||||
const data = customTransactionData || encodeToHexData(contractMethod, methodValues) || '0x'
|
||||
const to = getAddress(toAddress)
|
||||
const value = parseEther(tokenValue || '0').toString()
|
||||
|
||||
return {
|
||||
id: new Date().getTime(),
|
||||
contractInterface: contract,
|
||||
description: {
|
||||
to,
|
||||
value,
|
||||
customTransactionData,
|
||||
contractMethod,
|
||||
contractFieldsValues: methodValues,
|
||||
contractMethodIndex,
|
||||
nativeCurrencySymbol,
|
||||
networkPrefix,
|
||||
},
|
||||
raw: { to, value, data },
|
||||
}
|
||||
}
|
||||
|
||||
const SolidityForm = ({
|
||||
id,
|
||||
onSubmit,
|
||||
getAddressFromDomain,
|
||||
initialValues,
|
||||
nativeCurrencySymbol,
|
||||
networkPrefix,
|
||||
contract,
|
||||
children,
|
||||
showHexEncodedData,
|
||||
}: SolidityFormPropsTypes) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
watch,
|
||||
getValues,
|
||||
reset,
|
||||
formState: { isSubmitSuccessful, dirtyFields },
|
||||
} = useForm<SolidityFormValuesTypes>({
|
||||
defaultValues: initialValues,
|
||||
mode: 'onTouched', // This option allows you to configure the validation strategy before the user submits the form
|
||||
})
|
||||
|
||||
const toAddress = watch(TO_ADDRESS_FIELD_NAME)
|
||||
const contractMethodIndex = watch(CONTRACT_METHOD_INDEX_FIELD_NAME)
|
||||
const nativeValue = watch(NATIVE_VALUE_FIELD_NAME)
|
||||
const customTransactionData = watch(CUSTOM_TRANSACTION_DATA_FIELD_NAME)
|
||||
const contractMethod = contract?.methods[Number(contractMethodIndex)]
|
||||
|
||||
const contractFields = contractMethod?.inputs || []
|
||||
const showContractFields = !!contract && contract.methods.length > 0 && !showHexEncodedData
|
||||
const isPayableMethod = !!contract && contractMethod?.payable
|
||||
|
||||
const isValueInputVisible = showHexEncodedData || !showContractFields || isPayableMethod
|
||||
|
||||
useEffect(() => {
|
||||
const contractFieldsValues = getValues(CONTRACT_VALUES_FIELD_NAME)
|
||||
const methodValues = contractFieldsValues?.[`method-${contractMethodIndex}`]
|
||||
|
||||
if (showHexEncodedData && contractMethod) {
|
||||
const encodeData = encodeToHexData(contractMethod, methodValues)
|
||||
setValue(CUSTOM_TRANSACTION_DATA_FIELD_TYPE, encodeData || '')
|
||||
}
|
||||
}, [contractMethod, getValues, setValue, showHexEncodedData, contractMethodIndex])
|
||||
|
||||
// Resets form to initial values if the user edited contract method and then switched to custom data and edited it
|
||||
useEffect(() => {
|
||||
if (
|
||||
showHexEncodedData &&
|
||||
dirtyFields[CONTRACT_METHOD_INDEX_FIELD_NAME] &&
|
||||
dirtyFields[CUSTOM_TRANSACTION_DATA_FIELD_NAME]
|
||||
) {
|
||||
reset({
|
||||
...initialValues,
|
||||
[TO_ADDRESS_FIELD_NAME]: toAddress,
|
||||
[CUSTOM_TRANSACTION_DATA_FIELD_NAME]: customTransactionData,
|
||||
[NATIVE_VALUE_FIELD_NAME]: nativeValue,
|
||||
})
|
||||
}
|
||||
}, [dirtyFields, reset, showHexEncodedData, customTransactionData, toAddress, nativeValue, initialValues])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitSuccessful) {
|
||||
reset({ ...initialValues, [TO_ADDRESS_FIELD_NAME]: toAddress })
|
||||
}
|
||||
}, [isSubmitSuccessful, reset, toAddress, initialValues])
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id={id} data-testid={id} onSubmit={handleSubmit(onSubmit)} noValidate>
|
||||
{/* To Address field */}
|
||||
<Field
|
||||
id="to-address-input"
|
||||
name={TO_ADDRESS_FIELD_NAME}
|
||||
label="To Address"
|
||||
fullWidth
|
||||
required
|
||||
getAddressFromDomain={getAddressFromDomain}
|
||||
networkPrefix={networkPrefix}
|
||||
fieldType={ADDRESS_FIELD_TYPE}
|
||||
control={control}
|
||||
showErrorsInTheLabel={false}
|
||||
/>
|
||||
|
||||
{/* Native Token Amount Input */}
|
||||
{isValueInputVisible && (
|
||||
<Field
|
||||
id="token-value-input"
|
||||
name={NATIVE_VALUE_FIELD_NAME}
|
||||
label={`${nativeCurrencySymbol} value`}
|
||||
fieldType={NATIVE_AMOUNT_FIELD_TYPE}
|
||||
fullWidth
|
||||
required
|
||||
control={control}
|
||||
showErrorsInTheLabel={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Contract Section */}
|
||||
|
||||
{/* Contract Method Selector */}
|
||||
{showContractFields && (
|
||||
<Field
|
||||
id="contract-method-selector"
|
||||
name={CONTRACT_METHOD_INDEX_FIELD_NAME}
|
||||
label="Contract Method Selector"
|
||||
fieldType={CONTRACT_METHOD_FIELD_TYPE}
|
||||
shouldUnregister={false}
|
||||
control={control}
|
||||
options={contract?.methods.map((method, index) => ({
|
||||
id: index.toString(),
|
||||
label: method.name,
|
||||
}))}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Contract Fields */}
|
||||
{contractFields.map((contractField, index) => {
|
||||
const name = `${CONTRACT_VALUES_FIELD_NAME}.method-${contractMethodIndex}.${contractField.name || index}`
|
||||
const fieldType = getInputTypeHelper(contractField)
|
||||
|
||||
return (
|
||||
showContractFields && (
|
||||
<Field
|
||||
key={name}
|
||||
id={`contract-field-${contractField.name || index}`}
|
||||
name={name}
|
||||
label={`${contractField.name || `${index + 1}º contract field`} (${fieldType})`}
|
||||
fieldType={fieldType}
|
||||
fullWidth
|
||||
required
|
||||
shouldUnregister={false} // required to keep contract field values in the form state when the user switches between encoding and decoding data
|
||||
control={control}
|
||||
showErrorsInTheLabel={false}
|
||||
getAddressFromDomain={getAddressFromDomain}
|
||||
networkPrefix={networkPrefix}
|
||||
/>
|
||||
)
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Hex encoded textarea field */}
|
||||
{showHexEncodedData && (
|
||||
<Field
|
||||
id="hex-encoded-data"
|
||||
name={CUSTOM_TRANSACTION_DATA_FIELD_NAME}
|
||||
label="Data (Hex encoded)"
|
||||
fieldType={CUSTOM_TRANSACTION_DATA_FIELD_TYPE}
|
||||
required
|
||||
fullWidth
|
||||
control={control}
|
||||
showErrorsInTheLabel={false}
|
||||
/>
|
||||
)}
|
||||
{/* action buttons as a children */}
|
||||
{children}
|
||||
</form>
|
||||
|
||||
{/* set up the dev tool only in dev env */}
|
||||
{!isProdEnv() && <DevTool control={control} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SolidityForm
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ReactElement } from 'react'
|
||||
import AddressInput from './AddressInput'
|
||||
|
||||
interface AddressContractFieldProps {
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
label: string
|
||||
error?: string
|
||||
getAddressFromDomain?: (name: string) => Promise<string>
|
||||
networkPrefix?: string
|
||||
onBlur?: () => void
|
||||
}
|
||||
|
||||
const AddressContractField = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
error,
|
||||
getAddressFromDomain,
|
||||
networkPrefix,
|
||||
onBlur,
|
||||
}: AddressContractFieldProps): ReactElement => {
|
||||
return (
|
||||
<AddressInput
|
||||
id={id}
|
||||
name={name}
|
||||
label={label}
|
||||
address={value}
|
||||
inputProps={{ value }}
|
||||
onBlur={onBlur}
|
||||
showNetworkPrefix={!!networkPrefix}
|
||||
networkPrefix={networkPrefix}
|
||||
hiddenLabel={false}
|
||||
fullWidth
|
||||
error={error}
|
||||
getAddressFromDomain={getAddressFromDomain}
|
||||
onChangeAddress={onChange}
|
||||
showErrorsInTheLabel={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressContractField
|
||||
@@ -0,0 +1,204 @@
|
||||
import React, { ReactElement, useState, ChangeEvent, useEffect, useCallback, useRef } from 'react'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
import {
|
||||
addNetworkPrefix,
|
||||
checksumAddress,
|
||||
getAddressWithoutNetworkPrefix,
|
||||
getNetworkPrefix,
|
||||
isChecksumAddress,
|
||||
isValidAddress,
|
||||
isValidEnsName,
|
||||
} from '../../../utils/address'
|
||||
import TextFieldInput, { TextFieldInputProps } from './TextFieldInput'
|
||||
import useThrottle from '../../../hooks/useThrottle'
|
||||
|
||||
type AddressInputProps = {
|
||||
name: string
|
||||
address: string
|
||||
networkPrefix?: string
|
||||
showNetworkPrefix?: boolean
|
||||
defaultValue?: string
|
||||
disabled?: boolean
|
||||
onChangeAddress: (address: string) => void
|
||||
getAddressFromDomain?: (name: string) => Promise<string>
|
||||
customENSThrottleDelay?: number
|
||||
showLoadingSpinner?: boolean
|
||||
} & TextFieldInputProps
|
||||
|
||||
function AddressInput({
|
||||
name,
|
||||
address,
|
||||
networkPrefix,
|
||||
showNetworkPrefix = true,
|
||||
disabled,
|
||||
onChangeAddress,
|
||||
getAddressFromDomain,
|
||||
customENSThrottleDelay,
|
||||
showLoadingSpinner,
|
||||
InputProps,
|
||||
inputProps,
|
||||
hiddenLabel = false,
|
||||
...rest
|
||||
}: AddressInputProps): ReactElement {
|
||||
const [isLoadingENSResolution, setIsLoadingENSResolution] = useState(false)
|
||||
const defaultInputValue = addPrefix(address, networkPrefix, showNetworkPrefix)
|
||||
const inputRef = useRef({ value: defaultInputValue })
|
||||
const throttle = useThrottle()
|
||||
|
||||
// we checksum & include the network prefix in the input if showNetworkPrefix is set to true
|
||||
const updateInputValue = useCallback(
|
||||
(value = '') => {
|
||||
if (inputRef.current) {
|
||||
const checksumAddress = checksumValidAddress(value)
|
||||
inputRef.current.value = addPrefix(checksumAddress, networkPrefix, showNetworkPrefix)
|
||||
}
|
||||
},
|
||||
[networkPrefix, showNetworkPrefix],
|
||||
)
|
||||
|
||||
const resolveDomainName = useCallback(async () => {
|
||||
const isEnsName = isValidEnsName(address)
|
||||
|
||||
if (isEnsName && getAddressFromDomain) {
|
||||
try {
|
||||
setIsLoadingENSResolution(true)
|
||||
const resolvedAddress = await getAddressFromDomain(address)
|
||||
onChangeAddress(checksumValidAddress(resolvedAddress))
|
||||
// we update the input value
|
||||
updateInputValue(resolvedAddress)
|
||||
} catch (e) {
|
||||
onChangeAddress(address)
|
||||
} finally {
|
||||
setIsLoadingENSResolution(false)
|
||||
}
|
||||
}
|
||||
}, [address, getAddressFromDomain, onChangeAddress, updateInputValue])
|
||||
|
||||
// ENS name resolution
|
||||
useEffect(() => {
|
||||
if (getAddressFromDomain) {
|
||||
throttle(resolveDomainName, customENSThrottleDelay)
|
||||
}
|
||||
}, [getAddressFromDomain, resolveDomainName, customENSThrottleDelay, throttle])
|
||||
|
||||
// if address changes from outside (Like Loaded from a QR code) we update the input value
|
||||
useEffect(() => {
|
||||
const inputValue = inputRef.current?.value
|
||||
const inputWithoutPrefix = getAddressWithoutNetworkPrefix(inputValue)
|
||||
const addressWithoutPrefix = getAddressWithoutNetworkPrefix(address)
|
||||
const inputPrefix = getNetworkPrefix(inputValue)
|
||||
const addressPrefix = getNetworkPrefix(address)
|
||||
|
||||
const isNewAddressLoaded = inputWithoutPrefix !== addressWithoutPrefix
|
||||
const isNewPrefixLoaded = addressPrefix && inputPrefix !== addressPrefix
|
||||
|
||||
// we check if we load a new address (both prefixed and unprefixed cases)
|
||||
if (isNewAddressLoaded || isNewPrefixLoaded) {
|
||||
// we update the input value
|
||||
updateInputValue(address)
|
||||
}
|
||||
}, [address, updateInputValue])
|
||||
|
||||
// we trim, checksum & remove valid network prefix when a valid address is typed by the user
|
||||
const updateAddressState = useCallback(
|
||||
(value: string) => {
|
||||
const inputValue = value.trim()
|
||||
|
||||
const inputPrefix = getNetworkPrefix(inputValue)
|
||||
const inputWithoutPrefix = getAddressWithoutNetworkPrefix(inputValue)
|
||||
|
||||
// if the valid network prefix is present, we remove it from the address state
|
||||
const isValidPrefix = networkPrefix === inputPrefix
|
||||
const checksumAddress = checksumValidAddress(isValidPrefix ? inputWithoutPrefix : inputValue)
|
||||
|
||||
onChangeAddress(checksumAddress)
|
||||
},
|
||||
[networkPrefix, onChangeAddress],
|
||||
)
|
||||
|
||||
// when user switch the network we update the address state
|
||||
useEffect(() => {
|
||||
// Because the `address` is going to change after we call `updateAddressState`
|
||||
// To avoid calling `updateAddressState` twice, we check the value and the current address
|
||||
const inputValue = inputRef.current?.value
|
||||
if (inputValue !== address) {
|
||||
updateAddressState(inputRef.current?.value)
|
||||
}
|
||||
}, [networkPrefix, address, updateAddressState])
|
||||
|
||||
// when user types we update the address state
|
||||
function onChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
updateAddressState(e.target.value)
|
||||
}
|
||||
|
||||
const isLoading = isLoadingENSResolution || showLoadingSpinner
|
||||
|
||||
const [shrink, setshrink] = useState(!!defaultInputValue)
|
||||
|
||||
useEffect(() => {
|
||||
setshrink(!!inputRef.current?.value)
|
||||
}, [inputRef.current.value])
|
||||
|
||||
return (
|
||||
<TextFieldInput
|
||||
name={name}
|
||||
hiddenLabel={hiddenLabel && !shrink}
|
||||
disabled={disabled || isLoadingENSResolution}
|
||||
onChange={onChange}
|
||||
InputProps={{
|
||||
...InputProps,
|
||||
// if isLoading we show a custom loader adornment
|
||||
endAdornment: isLoading ? <LoaderSpinnerAdornment /> : InputProps?.endAdornment,
|
||||
}}
|
||||
inputProps={{
|
||||
...inputProps,
|
||||
ref: inputRef,
|
||||
}}
|
||||
InputLabelProps={{
|
||||
...rest.InputLabelProps,
|
||||
shrink: shrink || hiddenLabel || undefined,
|
||||
}}
|
||||
spellCheck={false}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressInput
|
||||
|
||||
function LoaderSpinnerAdornment() {
|
||||
return (
|
||||
<InputAdornment position="end">
|
||||
<CircularProgress size="16px" />
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
|
||||
// we only checksum valid addresses
|
||||
function checksumValidAddress(address: string) {
|
||||
if (isValidAddress(address) && !isChecksumAddress(address)) {
|
||||
return checksumAddress(address)
|
||||
}
|
||||
|
||||
return address
|
||||
}
|
||||
|
||||
// we try to add the network prefix if its not present
|
||||
function addPrefix(address: string, networkPrefix: string | undefined, showNetworkPrefix = false): string {
|
||||
if (!address) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (showNetworkPrefix && networkPrefix) {
|
||||
const hasPrefix = !!getNetworkPrefix(address)
|
||||
|
||||
// if the address has not prefix we add it by default
|
||||
if (!hasPrefix) {
|
||||
return addNetworkPrefix(address, networkPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
return address
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Control, Controller, FieldValues, FieldPath, PathValue } from 'react-hook-form'
|
||||
|
||||
export interface SelectItem {
|
||||
id: string
|
||||
label: string
|
||||
iconUrl?: string
|
||||
subLabel?: string
|
||||
}
|
||||
|
||||
import {
|
||||
BOOLEAN_FIELD_TYPE,
|
||||
CONTRACT_METHOD_FIELD_TYPE,
|
||||
CUSTOM_TRANSACTION_DATA_FIELD_TYPE,
|
||||
isAddressFieldType,
|
||||
isBooleanFieldType,
|
||||
} from './fields'
|
||||
import AddressContractField from './AddressContractField'
|
||||
import SelectContractField from './SelectContractField'
|
||||
import TextareaContractField from './TextareaContractField'
|
||||
import TextContractField from './TextContractField'
|
||||
import validateField, { ValidationFunction } from '../validations/validateField'
|
||||
|
||||
const CUSTOM_DEFAULT_VALUES: CustomDefaultValueTypes = {
|
||||
[BOOLEAN_FIELD_TYPE]: 'true',
|
||||
[CONTRACT_METHOD_FIELD_TYPE]: '0', // first contract method as default
|
||||
}
|
||||
|
||||
const BOOLEAN_DEFAULT_OPTIONS: SelectItem[] = [
|
||||
{ id: 'true', label: 'True' },
|
||||
{ id: 'false', label: 'False' },
|
||||
]
|
||||
|
||||
const DEFAULT_OPTIONS: DefaultOptionTypes = {
|
||||
[BOOLEAN_FIELD_TYPE]: BOOLEAN_DEFAULT_OPTIONS,
|
||||
}
|
||||
|
||||
interface CustomDefaultValueTypes {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
interface DefaultOptionTypes {
|
||||
[key: string]: SelectItem[]
|
||||
}
|
||||
|
||||
type FieldProps<T extends FieldValues = FieldValues> = {
|
||||
fieldType: string
|
||||
control: Control<T>
|
||||
id: string
|
||||
name: FieldPath<T> | string
|
||||
label: string
|
||||
fullWidth?: boolean
|
||||
required?: boolean
|
||||
validations?: ValidationFunction[]
|
||||
getAddressFromDomain?: (name: string) => Promise<string>
|
||||
networkPrefix?: string
|
||||
showErrorsInTheLabel?: boolean
|
||||
shouldUnregister?: boolean
|
||||
options?: SelectItem[]
|
||||
}
|
||||
|
||||
const Field = <T extends FieldValues = FieldValues>({
|
||||
fieldType,
|
||||
control,
|
||||
name,
|
||||
shouldUnregister = true,
|
||||
options,
|
||||
required = true,
|
||||
validations,
|
||||
...props
|
||||
}: FieldProps<T>) => {
|
||||
const FieldComponent = getFieldComponent(fieldType)
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={name as FieldPath<T>}
|
||||
control={control}
|
||||
defaultValue={(CUSTOM_DEFAULT_VALUES[fieldType] || '') as PathValue<T, FieldPath<T>>}
|
||||
shouldUnregister={shouldUnregister}
|
||||
rules={{
|
||||
required: {
|
||||
value: required,
|
||||
message: 'Required',
|
||||
},
|
||||
validate: validateField(fieldType, validations),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<FieldComponent
|
||||
name={field.name}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
value={field.value}
|
||||
options={options || DEFAULT_OPTIONS[fieldType]}
|
||||
error={fieldState.error?.message}
|
||||
required={required}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Field
|
||||
|
||||
interface FieldComponentBaseProps {
|
||||
name: string
|
||||
value: string
|
||||
onChange: (...event: unknown[]) => void
|
||||
onBlur?: () => void
|
||||
label: string
|
||||
error?: string
|
||||
required?: boolean
|
||||
options?: SelectItem[]
|
||||
id: string
|
||||
fullWidth?: boolean
|
||||
getAddressFromDomain?: (name: string) => Promise<string>
|
||||
networkPrefix?: string
|
||||
showErrorsInTheLabel?: boolean
|
||||
}
|
||||
|
||||
const getFieldComponent = (fieldType: string): React.FC<FieldComponentBaseProps> => {
|
||||
if (isAddressFieldType(fieldType)) {
|
||||
return AddressContractField as unknown as React.FC<FieldComponentBaseProps>
|
||||
}
|
||||
|
||||
if (isBooleanFieldType(fieldType)) {
|
||||
return SelectContractField as unknown as React.FC<FieldComponentBaseProps>
|
||||
}
|
||||
|
||||
if (fieldType === CONTRACT_METHOD_FIELD_TYPE) {
|
||||
return SelectContractField as unknown as React.FC<FieldComponentBaseProps>
|
||||
}
|
||||
|
||||
if (fieldType === CUSTOM_TRANSACTION_DATA_FIELD_TYPE) {
|
||||
return TextareaContractField as unknown as React.FC<FieldComponentBaseProps>
|
||||
}
|
||||
|
||||
return TextContractField as unknown as React.FC<FieldComponentBaseProps>
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { useState, useCallback, ClipboardEvent } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import { Box, Button, Tooltip } from '@mui/material'
|
||||
import useModal from '../../../hooks/useModal/useModal'
|
||||
import { Icon, IconTypes } from '../../Icon'
|
||||
import Text from '../../Text'
|
||||
import GenericModal from '../../GenericModal'
|
||||
import TextFieldInput from './TextFieldInput'
|
||||
|
||||
const DEFAULT_ROWS = 4
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
name: string
|
||||
label: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const JsonField = ({ id, name, label, value, onChange }: Props) => {
|
||||
const { open: showReplaceModal, toggleModal } = useModal()
|
||||
const [tempAbi, setTempAbi] = useState(value)
|
||||
const [isPrettified, setIsPrettified] = useState(false)
|
||||
const hasError = isValidJSON(value) ? undefined : 'Invalid JSON value'
|
||||
|
||||
const toggleFormatJSON = useCallback(() => {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
onChange(JSON.stringify(JSON.parse(value), null, isPrettified ? 0 : 2))
|
||||
setIsPrettified(!isPrettified)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
onChange(value)
|
||||
}
|
||||
}, [onChange, value, isPrettified])
|
||||
|
||||
const changeAbi = useCallback(() => {
|
||||
onChange(tempAbi)
|
||||
setIsPrettified(false)
|
||||
toggleModal()
|
||||
}, [tempAbi, onChange, toggleModal])
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(event: ClipboardEvent<HTMLInputElement>) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const clipboardData = event.clipboardData
|
||||
const pastedData = clipboardData?.getData('Text') || ''
|
||||
|
||||
if (value && pastedData) {
|
||||
setTempAbi(pastedData)
|
||||
toggleModal()
|
||||
} else {
|
||||
onChange(pastedData)
|
||||
}
|
||||
},
|
||||
[onChange, toggleModal, value],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<JSONFieldContainer>
|
||||
<StyledTextField
|
||||
id={id}
|
||||
name={name}
|
||||
label={label}
|
||||
multiline
|
||||
value={value}
|
||||
minRows={DEFAULT_ROWS}
|
||||
maxRows={DEFAULT_ROWS * 4}
|
||||
fullWidth
|
||||
hiddenLabel={false}
|
||||
onPaste={handlePaste}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value)
|
||||
}}
|
||||
spellCheck={false}
|
||||
showErrorsInTheLabel={false}
|
||||
error={hasError}
|
||||
/>
|
||||
|
||||
<IconContainer error={!!hasError}>
|
||||
{!isPrettified && (
|
||||
<IconContainerButton
|
||||
error={!!hasError}
|
||||
tooltipLabel="Prettify JSON"
|
||||
iconType="code"
|
||||
onClick={toggleFormatJSON}
|
||||
/>
|
||||
)}
|
||||
{isPrettified && (
|
||||
<IconContainerButton
|
||||
error={!!hasError}
|
||||
tooltipLabel="Stringify JSON"
|
||||
iconType="cross"
|
||||
onClick={toggleFormatJSON}
|
||||
/>
|
||||
)}
|
||||
</IconContainer>
|
||||
</JSONFieldContainer>
|
||||
|
||||
{showReplaceModal && (
|
||||
<GenericModal
|
||||
body={
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" width="100%">
|
||||
<Text variant="body1">Do you want to replace the current ABI?</Text>
|
||||
</Box>
|
||||
}
|
||||
onClose={toggleModal}
|
||||
title="Replace ABI"
|
||||
footer={
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" width="100%">
|
||||
<Button variant="outlined" onClick={toggleModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" onClick={changeAbi}>
|
||||
Accept
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const isValidJSON = (value: string | undefined) => {
|
||||
if (value) {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const IconContainerButton = ({
|
||||
tooltipLabel,
|
||||
iconType,
|
||||
onClick,
|
||||
error,
|
||||
}: {
|
||||
tooltipLabel: string
|
||||
iconType: IconTypes
|
||||
onClick: () => void
|
||||
error: boolean
|
||||
}) => (
|
||||
<Tooltip title={tooltipLabel}>
|
||||
<StyledButton size="small" color="primary" onClick={onClick}>
|
||||
<Icon size="sm" color={error ? 'error' : 'primary'} type={iconType} />
|
||||
</StyledButton>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
const JSONFieldContainer = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const IconContainer = styled.div<{ error: boolean }>`
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 15px;
|
||||
border: 1px solid ${({ theme, error }) => (error ? theme.palette.error.main : theme.palette.primary.main)};
|
||||
border-radius: 50%;
|
||||
background-color: ${({ theme }) => theme.palette.code.main};
|
||||
`
|
||||
|
||||
const StyledTextField = styled(TextFieldInput)`
|
||||
&& {
|
||||
textarea {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
&.MuiInputBase-input {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StyledButton = styled(IconButton)`
|
||||
margin: 0 5px;
|
||||
`
|
||||
|
||||
export default JsonField
|
||||
@@ -0,0 +1,56 @@
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
import { type SyntheticEvent, useCallback, useMemo } from 'react'
|
||||
import TextFieldInput from './TextFieldInput'
|
||||
|
||||
interface SelectItem {
|
||||
id: string
|
||||
label: string
|
||||
iconUrl?: string
|
||||
subLabel?: string
|
||||
}
|
||||
|
||||
type SelectContractFieldTypes = {
|
||||
options: SelectItem[]
|
||||
onChange: (id: string) => void
|
||||
value: string
|
||||
label: string
|
||||
name: string
|
||||
id: string
|
||||
}
|
||||
|
||||
const SelectContractField = ({ value, onChange, options, label, name, id }: SelectContractFieldTypes) => {
|
||||
const selectedValue = useMemo(() => options.find((opt) => opt.id === value), [options, value])
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(e: SyntheticEvent, value: SelectItem | null) => {
|
||||
if (value) {
|
||||
onChange(value.id)
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
id={id}
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={onValueChange}
|
||||
disabled={options.length === 1}
|
||||
renderInput={(params) => (
|
||||
<TextFieldInput
|
||||
{...params}
|
||||
label={label}
|
||||
name={name}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
id: `${id}-input`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectContractField
|
||||
@@ -0,0 +1,27 @@
|
||||
import styled from 'styled-components'
|
||||
import TextFieldInput, { TextFieldInputProps } from './TextFieldInput'
|
||||
|
||||
type TextContractFieldTypes = TextFieldInputProps & {
|
||||
networkPrefix?: undefined | string
|
||||
getAddressFromDomain?: () => void
|
||||
}
|
||||
|
||||
const TextContractField = ({
|
||||
networkPrefix: _networkPrefix,
|
||||
getAddressFromDomain: _getAddressFromDomain,
|
||||
...props
|
||||
}: TextContractFieldTypes) => {
|
||||
return <StyledTextField {...props} hiddenLabel={false} />
|
||||
}
|
||||
|
||||
export default TextContractField
|
||||
|
||||
const StyledTextField = styled(TextFieldInput)`
|
||||
&& {
|
||||
textarea {
|
||||
&.MuiInputBase-input {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import TextFieldMui, { TextFieldProps } from '@mui/material/TextField'
|
||||
import styled from 'styled-components'
|
||||
import { errorStyles, inputLabelStyles, inputStyles } from './styles'
|
||||
|
||||
export type TextFieldInputProps = {
|
||||
id?: string
|
||||
name: string
|
||||
label: string
|
||||
error?: string
|
||||
helperText?: string | undefined
|
||||
hiddenLabel?: boolean | undefined
|
||||
showErrorsInTheLabel?: boolean | undefined
|
||||
} & Omit<TextFieldProps, 'error'>
|
||||
|
||||
function TextFieldInput({
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
error = '',
|
||||
helperText,
|
||||
value,
|
||||
hiddenLabel,
|
||||
showErrorsInTheLabel,
|
||||
...rest
|
||||
}: TextFieldInputProps): ReactElement {
|
||||
const hasError = !!error
|
||||
|
||||
return (
|
||||
<TextField
|
||||
id={id || name}
|
||||
name={name}
|
||||
label={showErrorsInTheLabel && hasError ? error : label}
|
||||
value={value}
|
||||
helperText={!showErrorsInTheLabel && hasError ? error : helperText}
|
||||
error={hasError}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
hiddenLabel={hiddenLabel}
|
||||
InputLabelProps={{
|
||||
...rest.InputLabelProps,
|
||||
shrink: hiddenLabel || undefined,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TextField = styled((props: TextFieldProps) => <TextFieldMui {...props} />)<TextFieldProps>`
|
||||
&& {
|
||||
${inputLabelStyles}
|
||||
${inputStyles}
|
||||
${errorStyles}
|
||||
}
|
||||
`
|
||||
|
||||
export default TextFieldInput
|
||||