@ -0,0 +1,55 @@
|
||||
name: Run BATS Tests |
||||
|
||||
on: |
||||
push: |
||||
paths: |
||||
- 'docker-compose/setup-script/noco.sh' |
||||
workflow_dispatch: |
||||
|
||||
jobs: |
||||
prepare: |
||||
runs-on: ubuntu-latest |
||||
outputs: |
||||
matrix: ${{ steps.set-matrix.outputs.matrix }} |
||||
steps: |
||||
- name: Checkout code |
||||
uses: actions/checkout@v4 |
||||
|
||||
- name: Install jq |
||||
run: | |
||||
sudo apt-get update |
||||
sudo apt-get install -y jq |
||||
|
||||
- name: Prepare matrix for test files |
||||
id: set-matrix |
||||
run: | |
||||
BATS_FILES=$(find docker-compose/setup-script/tests -name '*.bats') |
||||
MATRIX_JSON=$(echo $BATS_FILES | jq -Rsc 'split("\n") | map(select(. != ""))') |
||||
echo "matrix=$MATRIX_JSON" >> $GITHUB_ENV |
||||
|
||||
test: |
||||
needs: prepare |
||||
runs-on: ubuntu-latest |
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
test: ${{fromJson(env.matrix)}} |
||||
steps: |
||||
- name: Checkout repository |
||||
uses: actions/checkout@v4 |
||||
|
||||
- name: Install BATS |
||||
run: | |
||||
sudo apt-get update |
||||
sudo apt-get install -y bats expect |
||||
|
||||
- name: Get working directory |
||||
run: | |
||||
WORKING_DIR="$(pwd)/docker-compose/setup-script/tests" |
||||
echo "WORKING_DIR=$WORKING_DIR" >> $GITHUB_ENV |
||||
|
||||
- name: Run BATS test |
||||
run: bats ${{ matrix.test }} |
||||
env: |
||||
WORKING_DIR: ${{ env.WORKING_DIR }} |
||||
SKIP_TARE_DOWN: true |
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/configure" || exit 1 |
||||
./setup.sh "setup" |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Properly runs monitor script" { |
||||
../expects/configure/restart.sh |
||||
|
||||
cd "${NOCO_HOME}" || exit 1 |
||||
|
||||
# Verify container is running |
||||
docker compose ps | grep -q 'redis' |
||||
docker compose ps | grep -q 'watchtower' |
||||
docker compose ps | grep -q 'nocodb' |
||||
} |
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/configure" || exit 1 |
||||
./setup.sh "setup" |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check all containers are restarted" { |
||||
../expects/configure/restart.sh |
||||
|
||||
cd "${NOCO_HOME}" || exit 1 |
||||
|
||||
# Verify container is running |
||||
docker compose ps | grep -q 'redis' |
||||
docker compose ps | grep -q 'watchtower' |
||||
docker compose ps | grep -q 'nocodb' |
||||
} |
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/configure" || exit 1 |
||||
./setup.sh "setup" |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check NocoDB is scaled to 3 instances" { |
||||
nproc() { |
||||
echo 4 |
||||
} |
||||
|
||||
../expects/configure/scale.sh |
||||
|
||||
cd "${NOCO_HOME}" || exit 1 |
||||
|
||||
result=$(docker compose ps | grep -c "nocodb/nocodb") |
||||
[ "${result}" -eq 3 ] |
||||
} |
@ -0,0 +1,17 @@
|
||||
#!/bin/bash |
||||
|
||||
if [ -z "$NOCO_HOME" ]; then |
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
fi |
||||
|
||||
if [ -d "$NOCO_HOME" ]; then |
||||
cd "$NOCO_HOME" || exit |
||||
docker compose down |
||||
fi |
||||
|
||||
cd "$WORKING_DIR" || exit |
||||
rm -rf "$NOCO_HOME" |
||||
|
||||
if [ "$1" = "setup" ]; then |
||||
../noco.sh <<< $'\n\nN\n' |
||||
fi |
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/configure" || exit 1 |
||||
./setup.sh "setup" |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check all containers are up" { |
||||
../expects/configure/start.sh |
||||
|
||||
cd "${NOCO_HOME}" || exit 1 |
||||
|
||||
# Verify container is running |
||||
docker compose ps | grep -q 'redis' |
||||
docker compose ps | grep -q 'watchtower' |
||||
docker compose ps | grep -q 'nocodb' |
||||
} |
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/configure" || exit 1 |
||||
./setup.sh setup |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check all containers are down" { |
||||
../expects/configure/stop.sh |
||||
|
||||
cd "${NOCO_HOME}" || exit 1 |
||||
|
||||
# Verify container is not running |
||||
count=$(docker compose ps -q | wc -l) |
||||
[ "$count" -eq 0 ] |
||||
} |
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/configure" || exit 1 |
||||
./setup.sh "setup" |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check all containers are upgraded" { |
||||
../expects/configure/upgrade.sh |
||||
|
||||
cd "${NOCO_HOME}" || exit 1 |
||||
|
||||
# Verify container is running |
||||
docker compose ps | grep -q 'redis' |
||||
docker compose ps | grep -q 'watchtower' |
||||
docker compose ps | grep -q 'nocodb' |
||||
} |
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
expect "Do you want to reinstall NocoDB*" |
||||
send "N\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "7\r" |
||||
|
||||
send \x03 |
||||
|
||||
expect "Enter your choice: " |
||||
send "0\r" |
||||
|
||||
expect EOF |
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
expect "Do you want to reinstall NocoDB*" |
||||
send "N\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "4\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "0\r" |
||||
|
||||
expect EOF |
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
expect "Do you want to reinstall NocoDB*" |
||||
send "N\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "6\r" |
||||
|
||||
expect "How many instances of NocoDB do you want to run*" |
||||
send "3\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "0\r" |
||||
|
||||
expect EOF |
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
expect "Do you want to reinstall NocoDB*" |
||||
send "N\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "1\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "0\r" |
||||
|
||||
expect EOF |
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
expect "Do you want to reinstall NocoDB*" |
||||
send "N\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "2\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "0\r" |
||||
|
||||
expect EOF |
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
expect "Do you want to reinstall NocoDB*" |
||||
send "N\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "5\r" |
||||
|
||||
expect "Enter your choice: " |
||||
send "0\r" |
||||
|
||||
expect EOF |
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
# Respond to script prompts |
||||
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||
send "\r" |
||||
|
||||
expect "Show Advanced Options*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to start the management menu*" |
||||
send "N\r" |
||||
|
||||
expect eof |
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/expect -f |
||||
# shellcheck shell=bash |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
# Respond to script prompts |
||||
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||
send "192.168.1.10\r" |
||||
|
||||
expect "Show Advanced Options*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to start the management menu*" |
||||
send "N\r" |
||||
|
||||
expect eof |
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
# Respond to script prompts |
||||
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||
send "\r" |
||||
|
||||
expect "Show Advanced Options*" |
||||
send "Y\r" |
||||
|
||||
expect "Choose Community or Enterprise Edition*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to enabled Redis for caching*" |
||||
send "Y\r" |
||||
|
||||
expect "Do you want to enabled Watchtower for automatic updates*" |
||||
send "\r" |
||||
|
||||
expect "How many instances of NocoDB do you want to run*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to start the management menu*" |
||||
send "N\r" |
||||
|
||||
expect eof |
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
# Respond to script prompts |
||||
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||
send "\r" |
||||
|
||||
expect "Show Advanced Options*" |
||||
send "Y\r" |
||||
|
||||
expect "Choose Community or Enterprise Edition*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to enabled Redis for caching*" |
||||
send "Y\r" |
||||
|
||||
expect "Do you want to enabled Watchtower for automatic updates*" |
||||
send "\r" |
||||
|
||||
expect "How many instances of NocoDB do you want to run*" |
||||
send "2\r" |
||||
|
||||
expect "Do you want to start the management menu*" |
||||
send "N\r" |
||||
|
||||
expect eof |
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
set random_number [lindex $argv 0] |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
# Respond to script prompts |
||||
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||
send "${random_number}.ssl.nocodb.dev\r" |
||||
|
||||
expect "Show Advanced Options*" |
||||
send "y\r" |
||||
|
||||
expect "Do you want to configure SSL*" |
||||
send "y\r" |
||||
|
||||
expect "Choose Community or Enterprise Edition*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to enabled Redis for caching*" |
||||
send "Y\r" |
||||
|
||||
expect "Do you want to enabled Watchtower for automatic updates*" |
||||
send "\r" |
||||
|
||||
expect "How many instances of NocoDB do you want to run*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to start the management menu*" |
||||
send "N\r" |
||||
|
||||
expect eof |
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/expect -f |
||||
|
||||
# Configure timeout for each expect command |
||||
set timeout 10 |
||||
|
||||
# Start your main script |
||||
set env(PATH) "$env(WORKING_DIR)/mocks:$env(PATH)" |
||||
|
||||
spawn bash ../../noco.sh |
||||
|
||||
# Respond to script prompts |
||||
expect "Enter the IP address or domain name for the NocoDB instance (default: localhost):" |
||||
send "\r" |
||||
|
||||
expect "Show Advanced Options*" |
||||
send "Y\r" |
||||
|
||||
expect "Choose Community or Enterprise Edition*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to enabled Redis for caching*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to enabled Watchtower for automatic updates*" |
||||
send "Y\r" |
||||
|
||||
expect "How many instances of NocoDB do you want to run*" |
||||
send "\r" |
||||
|
||||
expect "Do you want to start the management menu*" |
||||
send "N\r" |
||||
|
||||
expect eof |
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check installation with all default options" { |
||||
../expects/install/default.sh |
||||
|
||||
cd "${NOCO_HOME}" |
||||
|
||||
# Check Docker Compose file to verify configuration |
||||
grep -q 'redis' docker-compose.yml |
||||
grep -q 'watchtower' docker-compose.yml |
||||
grep -q 'nocodb' docker-compose.yml |
||||
|
||||
# Verify container is running |
||||
docker compose ps | grep -q 'redis' |
||||
docker compose ps | grep -q 'watchtower' |
||||
docker compose ps | grep -q 'nocodb' |
||||
} |
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check installation with custom ip" { |
||||
../expects/install/ip.sh |
||||
|
||||
cd "${NOCO_HOME}" |
||||
|
||||
# Check Docker Compose file to verify configuration |
||||
grep -q 'redis' docker-compose.yml |
||||
grep -q 'watchtower' docker-compose.yml |
||||
grep -q 'nocodb' docker-compose.yml |
||||
|
||||
# Verify container is running |
||||
docker compose ps | grep -q 'redis' |
||||
docker compose ps | grep -q 'watchtower' |
||||
docker compose ps | grep -q 'nocodb' |
||||
} |
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check Redis is enabled when specified" { |
||||
../expects/install/redis.sh |
||||
|
||||
cd "${NOCO_HOME}" |
||||
|
||||
# Check Docker Compose file to verify Redis configuration |
||||
grep -q 'redis' docker-compose.yml |
||||
|
||||
# Verify Redis container is running |
||||
docker compose ps | grep -q 'redis' |
||||
} |
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
|
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check if two instances of NoCoDB can be run" { |
||||
../expects/install/scale.sh |
||||
|
||||
cd "${NOCO_HOME}" |
||||
|
||||
# Get scale from docker compose ps |
||||
scale=$(docker compose ps | grep -c "nocodb/nocodb") |
||||
[ "$scale" -eq 2 ] |
||||
} |
@ -0,0 +1,12 @@
|
||||
#!/bin/bash |
||||
|
||||
if [ -z "$NOCO_HOME" ]; then |
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
fi |
||||
|
||||
if [ -d "$NOCO_HOME" ]; then |
||||
cd "$NOCO_HOME" || exit |
||||
docker compose down |
||||
fi |
||||
|
||||
rm -rf "$NOCO_HOME" |
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
|
||||
|
||||
RANDOM_NUMBER=$RANDOM |
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Should create SSL certificates" { |
||||
if [ -z "$TEST_SSL" ] |
||||
then |
||||
skip "Skipping SSL tests" |
||||
fi |
||||
|
||||
../expects/install/ssl.sh "$RANDOM_NUMBER" |
||||
|
||||
curl -ksS --head "https://${RANDOM_NUMBER}.ssl.nocodb.dev" > /dev/null |
||||
} |
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bats |
||||
|
||||
NOCO_HOME="${HOME}/.nocodb" |
||||
export NOCO_HOME |
||||
|
||||
setup() { |
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
teardown() { |
||||
if [ -n "$SKIP_TEARDOWN" ]; then |
||||
return |
||||
fi |
||||
|
||||
cd "${WORKING_DIR}/install" || exit 1 |
||||
./setup.sh |
||||
} |
||||
|
||||
@test "Check WatchTower is enabled when specified" { |
||||
../expects/install/watchtower.sh |
||||
|
||||
cd "${NOCO_HOME}" |
||||
|
||||
# Check Docker Compose file to verify WatchTower configuration |
||||
grep -q 'watchtower' docker-compose.yml |
||||
|
||||
# Verify WatchTower container is running |
||||
docker compose ps | grep -q 'watchtower' |
||||
} |
@ -0,0 +1,3 @@
|
||||
#!/bin/bash |
||||
|
||||
echo "--- Clear Mock ---" |
@ -0,0 +1,3 @@
|
||||
#!/bin/bash |
||||
|
||||
echo 4 |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 377 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 732 B |
After Width: | Height: | Size: 347 B |
After Width: | Height: | Size: 257 B |
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts"> |
||||
const { header, field, toggleSort } = defineProps<{ |
||||
header: string |
||||
activeSort: { field?: string; direction?: string } |
||||
field: UsersSortType['field'] |
||||
toggleSort: Function |
||||
}>() |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex items-center space-x-2 cursor-pointer text-gray-700" @click="toggleSort(field)"> |
||||
<span> |
||||
{{ header }} |
||||
</span> |
||||
<div class="flex flex-col"> |
||||
<GeneralIcon |
||||
icon="arrowDropUp" |
||||
class="text-sm mb-[-10px] text-[16px]" |
||||
:class="{ |
||||
'text-primary': activeSort.field === field && activeSort.direction === 'asc', |
||||
}" |
||||
/> |
||||
<GeneralIcon |
||||
icon="arrowDropDown" |
||||
class="text-sm text-[16px]" |
||||
:class="{ |
||||
'text-primary': activeSort.field === field && activeSort.direction === 'desc', |
||||
}" |
||||
/> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -1,79 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { iconMap } from '#imports' |
||||
import type { UsersSortType } from '~/lib' |
||||
|
||||
const { field, direction, handleUserSort } = defineProps<{ |
||||
field: UsersSortType['field'] |
||||
direction?: UsersSortType['direction'] |
||||
handleUserSort: Function |
||||
}>() |
||||
|
||||
const isOpen = ref(false) |
||||
|
||||
const sortUserBy = (direction?: UsersSortType['direction']) => { |
||||
handleUserSort({ |
||||
field, |
||||
direction, |
||||
}) |
||||
isOpen.value = false |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-dropdown |
||||
v-model:visible="isOpen" |
||||
:trigger="['click']" |
||||
placement="bottomLeft" |
||||
overlay-class-name="nc-user-menu-column-operations !border-1 rounded-lg !shadow-xl" |
||||
@click.stop="isOpen = !isOpen" |
||||
> |
||||
<div> |
||||
<GeneralIcon |
||||
:icon="direction === 'asc' || direction === 'desc' ? 'sortDesc' : 'arrowDown'" |
||||
class="text-grey h-full text-grey nc-user-menu-trigger cursor-pointer outline-0 mr-2 transition-none" |
||||
:style="{ transform: direction === 'asc' ? 'rotate(180deg)' : undefined }" |
||||
/> |
||||
</div> |
||||
<template #overlay> |
||||
<NcMenu class="flex flex-col gap-1 border-gray-200 nc-user-menu-column-options"> |
||||
<NcMenuItem @click="sortUserBy('asc')"> |
||||
<div class="nc-column-insert-after nc-user-menu-item"> |
||||
<component |
||||
:is="iconMap.sortDesc" |
||||
class="text-gray-700 !rotate-180 !w-4.25 !h-4.25" |
||||
:style="{ |
||||
transform: 'rotate(180deg)', |
||||
}" |
||||
/> |
||||
|
||||
<!-- Sort Ascending --> |
||||
{{ $t('general.sortAsc') }} |
||||
</div> |
||||
</NcMenuItem> |
||||
<NcMenuItem @click="sortUserBy('desc')"> |
||||
<div class="nc-column-insert-before nc-user-menu-item"> |
||||
<component :is="iconMap.sortDesc" class="text-gray-700 !w-4.25 !h-4.25 ml-0.5 mr-0.25" /> |
||||
<!-- Sort Descending --> |
||||
{{ $t('general.sortDesc') }} |
||||
</div> |
||||
</NcMenuItem> |
||||
</NcMenu> |
||||
</template> |
||||
</a-dropdown> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.nc-user-menu-item { |
||||
@apply flex items-center gap-2; |
||||
} |
||||
|
||||
.nc-user-menu-column-options { |
||||
.nc-icons { |
||||
@apply !w-5 !h-5; |
||||
} |
||||
} |
||||
|
||||
:deep(.ant-dropdown-menu-item) { |
||||
@apply !hover:text-black text-gray-700; |
||||
} |
||||
</style> |
@ -0,0 +1,3 @@
|
||||
<template> |
||||
<span></span> |
||||
</template> |
@ -0,0 +1,110 @@
|
||||
<script lang="ts" setup> |
||||
import { useVModel } from '#imports' |
||||
|
||||
interface Prop { |
||||
modelValue: boolean |
||||
extensionId: string |
||||
from: 'market' | 'extension' |
||||
} |
||||
|
||||
const props = defineProps<Prop>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emit) |
||||
|
||||
const { availableExtensions, addExtension, getExtensionIcon, isMarketVisible } = useExtensions() |
||||
|
||||
const onBack = () => { |
||||
vModel.value = false |
||||
isMarketVisible.value = true |
||||
} |
||||
|
||||
const onAddExtension = (ext: any) => { |
||||
addExtension(ext) |
||||
vModel.value = false |
||||
} |
||||
|
||||
const activeExtension = computed(() => { |
||||
return availableExtensions.value.find((ext) => ext.id === props.extensionId) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<NcModal |
||||
v-model:visible="vModel" |
||||
:body-style="{ 'max-height': '864px', 'height': '85vh' }" |
||||
:class="{ active: vModel }" |
||||
:closable="from === 'extension'" |
||||
:footer="null" |
||||
:width="1280" |
||||
size="medium" |
||||
wrap-class-name="nc-modal-extension-market" |
||||
> |
||||
<div v-if="activeExtension" class="flex flex-col w-full h-full"> |
||||
<div v-if="from === 'market'" class="h-[40px] flex items-start"> |
||||
<div class="flex items-center gap-2 pr-2 pb-2 cursor-pointer hover:text-primary" @click="onBack"> |
||||
<GeneralIcon icon="ncArrowLeft" /> |
||||
<span>Back</span> |
||||
</div> |
||||
</div> |
||||
<div v-else class="h-[40px]"></div> |
||||
<div class="extension-details"> |
||||
<div class="extension-details-left"> |
||||
<div class="flex"> |
||||
<img :src="getExtensionIcon(activeExtension.iconUrl)" alt="icon" class="h-[90px]" /> |
||||
<div class="flex flex-col p-4"> |
||||
<div class="font-weight-700 text-2xl">{{ activeExtension.title }}</div> |
||||
</div> |
||||
</div> |
||||
<div class="p-4"> |
||||
<div class="whitespace-pre-line">{{ activeExtension.description }}</div> |
||||
</div> |
||||
</div> |
||||
<div class="extension-details-right"> |
||||
<NcButton class="w-full" @click="onAddExtension(activeExtension)"> |
||||
<div class="flex items-center justify-center">Add Extension</div> |
||||
</NcButton> |
||||
<div class="flex flex-col gap-1"> |
||||
<div class="text-md font-weight-600">Version</div> |
||||
<div>{{ activeExtension.version }}</div> |
||||
</div> |
||||
<div class="flex flex-col gap-1"> |
||||
<div v-if="activeExtension.publisherName" class="text-md font-weight-600">Publisher</div> |
||||
<div>{{ activeExtension.publisherName }}</div> |
||||
</div> |
||||
<div v-if="activeExtension.publisherEmail" class="flex flex-col gap-1"> |
||||
<div class="text-md font-weight-600">Publisher Email</div> |
||||
<div> |
||||
<a :href="`mailto:${activeExtension.publisherEmail}`" target="_blank" rel="noopener noreferrer"> |
||||
{{ activeExtension.publisherEmail }} |
||||
</a> |
||||
</div> |
||||
</div> |
||||
<div v-if="activeExtension.publisherUrl" class="flex flex-col gap-1"> |
||||
<div class="text-md font-weight-600">Publisher Website</div> |
||||
<div> |
||||
<a :href="activeExtension.publisherUrl" target="_blank" rel="noopener noreferrer"> |
||||
{{ activeExtension.publisherUrl }} |
||||
</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</NcModal> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.extension-details { |
||||
@apply flex w-full h-full; |
||||
|
||||
.extension-details-left { |
||||
@apply flex flex-col w-3/4 p-2; |
||||
} |
||||
|
||||
.extension-details-right { |
||||
@apply w-1/4 p-2 flex flex-col gap-4; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,231 @@
|
||||
<script setup lang="ts"> |
||||
interface Prop { |
||||
extensionId: string |
||||
error?: any |
||||
} |
||||
|
||||
const { extensionId, error } = defineProps<Prop>() |
||||
|
||||
const { extensionList, extensionsLoaded, availableExtensions, getExtensionIcon, duplicateExtension, showExtensionDetails } = |
||||
useExtensions() |
||||
|
||||
const activeError = ref(error) |
||||
|
||||
const extensionModalRef = ref<HTMLElement>() |
||||
|
||||
const extension = computed(() => { |
||||
const ext = extensionList.value.find((ext) => ext.id === extensionId) |
||||
if (!ext) { |
||||
throw new Error('Extension not found') |
||||
} |
||||
return ext |
||||
}) |
||||
|
||||
const titleInput = ref<HTMLInputElement | null>(null) |
||||
|
||||
const titleEditMode = ref<boolean>(false) |
||||
|
||||
const tempTitle = ref<string>(extension.value.title) |
||||
|
||||
const enableEditMode = () => { |
||||
titleEditMode.value = true |
||||
tempTitle.value = extension.value.title |
||||
nextTick(() => { |
||||
titleInput.value?.focus() |
||||
titleInput.value?.select() |
||||
titleInput.value?.scrollIntoView() |
||||
}) |
||||
} |
||||
|
||||
const updateExtensionTitle = async () => { |
||||
await extension.value.setTitle(tempTitle.value) |
||||
titleEditMode.value = false |
||||
} |
||||
|
||||
const { fullscreen, collapsed } = useProvideExtensionHelper(extension) |
||||
|
||||
const component = ref<any>(null) |
||||
|
||||
const extensionManifest = ref<any>(null) |
||||
|
||||
onMounted(() => { |
||||
until(extensionsLoaded) |
||||
.toMatch((v) => v) |
||||
.then(() => { |
||||
extensionManifest.value = availableExtensions.value.find((ext) => ext.id === extension.value.extensionId) |
||||
|
||||
if (!extensionManifest) { |
||||
return |
||||
} |
||||
|
||||
import(`../../extensions/${extensionManifest.value.entry}/index.vue`).then((mod) => { |
||||
component.value = markRaw(mod.default) |
||||
}) |
||||
}) |
||||
.catch((err) => { |
||||
if (!extensionManifest.value) { |
||||
activeError.value = 'There was an error loading the extension' |
||||
return |
||||
} |
||||
activeError.value = err |
||||
}) |
||||
}) |
||||
|
||||
// close fullscreen on escape key press |
||||
useEventListener('keydown', (e) => { |
||||
if (e.key === 'Escape') { |
||||
fullscreen.value = false |
||||
} |
||||
}) |
||||
|
||||
// close fullscreen on clicking extensionModalRef directly |
||||
const closeFullscreen = (e: MouseEvent) => { |
||||
if (e.target === extensionModalRef.value) { |
||||
fullscreen.value = false |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="w-full p-2"> |
||||
<div class="extension-wrapper"> |
||||
<div class="extension-header"> |
||||
<div class="extension-header-left"> |
||||
<GeneralIcon icon="drag" /> |
||||
<img v-if="extensionManifest" :src="getExtensionIcon(extensionManifest.iconUrl)" alt="icon" class="h-6" /> |
||||
<input |
||||
v-if="titleEditMode" |
||||
ref="titleInput" |
||||
v-model="tempTitle" |
||||
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5" |
||||
@click.stop |
||||
@keyup.enter="updateExtensionTitle" |
||||
@keyup.esc="updateExtensionTitle" |
||||
@blur="updateExtensionTitle" |
||||
/> |
||||
<div v-else class="extension-title" @dblclick="enableEditMode">{{ extension.title }}</div> |
||||
</div> |
||||
<div class="extension-header-right"> |
||||
<GeneralIcon v-if="!activeError" icon="expand" @click="fullscreen = true" /> |
||||
<NcDropdown :trigger="['click']"> |
||||
<GeneralIcon icon="threeDotVertical" /> |
||||
|
||||
<template #overlay> |
||||
<NcMenu> |
||||
<template v-if="!activeError"> |
||||
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="enableEditMode"> |
||||
<GeneralIcon icon="edit" /> |
||||
Rename |
||||
</NcMenuItem> |
||||
<NcMenuItem data-rec="true" class="!hover:text-primary" @click="duplicateExtension(extension.id)"> |
||||
<GeneralIcon icon="duplicate" /> |
||||
Duplicate |
||||
</NcMenuItem> |
||||
<NcMenuItem |
||||
data-rec="true" |
||||
class="!hover:text-primary" |
||||
@click="showExtensionDetails(extension.extensionId, 'extension')" |
||||
> |
||||
<GeneralIcon icon="info" /> |
||||
Details |
||||
</NcMenuItem> |
||||
<NcDivider /> |
||||
</template> |
||||
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="extension.clear()"> |
||||
<GeneralIcon icon="reload" /> |
||||
Clear Data |
||||
</NcMenuItem> |
||||
<NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="extension.delete()"> |
||||
<GeneralIcon icon="delete" /> |
||||
Delete |
||||
</NcMenuItem> |
||||
</NcMenu> |
||||
</template> |
||||
</NcDropdown> |
||||
<GeneralIcon v-if="collapsed" icon="arrowUp" @click="collapsed = !collapsed" /> |
||||
<GeneralIcon v-else icon="arrowDown" @click="collapsed = !collapsed" /> |
||||
</div> |
||||
</div> |
||||
<template v-if="activeError"> |
||||
<div v-show="!collapsed" class="extension-content"> |
||||
<a-result status="error" title="Extension Error"> |
||||
<template #subTitle>{{ activeError }}</template> |
||||
<template #extra> |
||||
<NcButton @click="extension.clear()"> |
||||
<div class="flex items-center gap-2"> |
||||
<GeneralIcon icon="reload" /> |
||||
Clear Data |
||||
</div> |
||||
</NcButton> |
||||
<NcButton type="danger" @click="extension.delete()"> |
||||
<div class="flex items-center gap-2"> |
||||
<GeneralIcon icon="delete" /> |
||||
Delete |
||||
</div> |
||||
</NcButton> |
||||
</template> |
||||
</a-result> |
||||
</div> |
||||
</template> |
||||
<template v-else> |
||||
<Teleport to="body" :disabled="!fullscreen"> |
||||
<div ref="extensionModalRef" :class="{ 'extension-modal': fullscreen }" @click="closeFullscreen"> |
||||
<div :class="{ 'extension-modal-content': fullscreen }"> |
||||
<div |
||||
v-if="fullscreen" |
||||
class="flex items-center justify-between p-2 bg-gray-100 rounded-t-lg cursor-default h-[40px]" |
||||
> |
||||
<div class="flex items-center gap-2 text-gray-500 font-weight-600"> |
||||
<img v-if="extensionManifest" :src="getExtensionIcon(extensionManifest.iconUrl)" alt="icon" class="w-6 h-6" /> |
||||
<div class="text-sm">{{ extension.title }}</div> |
||||
</div> |
||||
<GeneralIcon class="cursor-pointer" icon="close" @click="fullscreen = false" /> |
||||
</div> |
||||
<div |
||||
v-show="fullscreen || !collapsed" |
||||
class="extension-content" |
||||
:class="{ 'border-1': !fullscreen, 'h-[calc(100%-40px)]': fullscreen }" |
||||
> |
||||
<component :is="component" :key="extension.uiKey" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</Teleport> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.extension-wrapper { |
||||
@apply bg-white rounded-lg p-2 w-full border-1; |
||||
} |
||||
|
||||
.extension-header { |
||||
@apply flex justify-between mb-2; |
||||
|
||||
.extension-header-left { |
||||
@apply flex items-center gap-2; |
||||
} |
||||
|
||||
.extension-header-right { |
||||
@apply flex items-center gap-4; |
||||
} |
||||
|
||||
.extension-title { |
||||
@apply font-weight-600; |
||||
} |
||||
} |
||||
|
||||
.extension-content { |
||||
@apply rounded-lg; |
||||
} |
||||
|
||||
.extension-modal { |
||||
@apply absolute top-0 left-0 z-50 w-full h-full bg-black bg-opacity-50; |
||||
|
||||
.extension-modal-content { |
||||
@apply bg-white rounded-lg w-[90%] h-[90vh] mt-[5vh] mx-auto; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup> |
||||
import { useVModel } from '#imports' |
||||
|
||||
interface Prop { |
||||
modelValue?: boolean |
||||
} |
||||
|
||||
const props = defineProps<Prop>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const vModel = useVModel(props, 'modelValue', emit) |
||||
|
||||
const { availableExtensions, addExtension, getExtensionIcon, showExtensionDetails } = useExtensions() |
||||
|
||||
const onExtensionClick = (extensionId: string) => { |
||||
showExtensionDetails(extensionId) |
||||
vModel.value = false |
||||
} |
||||
|
||||
const onAddExtension = (ext: any) => { |
||||
addExtension(ext) |
||||
vModel.value = false |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<NcModal |
||||
v-model:visible="vModel" |
||||
:body-style="{ 'max-height': '864px', 'height': '85vh' }" |
||||
:class="{ active: vModel }" |
||||
:closable="true" |
||||
:footer="null" |
||||
:width="1280" |
||||
size="medium" |
||||
wrap-class-name="nc-modal-extension-market" |
||||
> |
||||
<div class="flex flex-col h-full"> |
||||
<div class="flex items-center px-4 py-2"> |
||||
<div class="flex items-center gap-2"> |
||||
<GeneralIcon icon="puzzle" /> |
||||
<div class="font-weight-700">Extensions Marketplace</div> |
||||
</div> |
||||
</div> |
||||
<div class="flex flex-col flex-1 px-4 py-2"> |
||||
<div class="flex flex-wrap gap-4 p-2"> |
||||
<template v-for="ext of availableExtensions" :key="ext.id"> |
||||
<div class="flex border-1 rounded-lg p-2 w-[360px] cursor-pointer" @click="onExtensionClick(ext.id)"> |
||||
<div class="h-[60px] overflow-hidden m-auto"> |
||||
<img :src="getExtensionIcon(ext.iconUrl)" alt="icon" class="w-full h-full object-cover" /> |
||||
</div> |
||||
<div class="flex flex-grow flex-col ml-3"> |
||||
<div class="flex justify-between"> |
||||
<div class="font-weight-600">{{ ext.title }}</div> |
||||
<NcButton size="xsmall" @click.stop="onAddExtension(ext)"> |
||||
<div class="flex items-center gap-1 mx-1"> |
||||
<GeneralIcon icon="plus" /> |
||||
Add |
||||
</div> |
||||
</NcButton> |
||||
</div> |
||||
<div class="w-[250px] h-[50px] text-xs line-clamp-3">{{ ext.description }}</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</NcModal> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,59 @@
|
||||
<script setup lang="ts"> |
||||
import { Pane } from 'splitpanes' |
||||
import 'splitpanes/dist/splitpanes.css' |
||||
|
||||
const { extensionList, isPanelExpanded, isDetailsVisible, detailsExtensionId, detailsFrom, isMarketVisible, extensionPanelSize } = |
||||
useExtensions() |
||||
|
||||
const toggleMarket = () => { |
||||
isMarketVisible.value = !isMarketVisible.value |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<Pane v-if="isPanelExpanded" :size="extensionPanelSize" class="flex flex-col bg-orange-50"> |
||||
<div class="flex items-center pl-3 pt-3 font-weight-800 text-orange-500">Extensions</div> |
||||
<template v-if="extensionList.length === 0"> |
||||
<div class="flex items-center flex-col gap-2 w-full nc-scrollbar-md"> |
||||
<div class="w-[100px] h-[100px] bg-gray-200 rounded-lg mt-[100px]"></div> |
||||
<div class="font-weight-700">No extensions added</div> |
||||
<div>Add Extensions from the community extensions marketplace</div> |
||||
<NcButton @click="toggleMarket"> |
||||
<div class="flex items-center gap-2 font-weight-600"> |
||||
<GeneralIcon icon="plus" /> |
||||
Add Extension |
||||
</div> |
||||
</NcButton> |
||||
</div> |
||||
</template> |
||||
<template v-else> |
||||
<div class="flex w-full items-center justify-between py-2 px-2 bg-orange-50"> |
||||
<div class="flex flex-grow items-center mr-2"> |
||||
<a-input type="text" class="!h-8 !px-3 !py-1 !rounded-lg" placeholder="Search Extension"> |
||||
<template #prefix> |
||||
<GeneralIcon icon="search" class="mr-2 h-4 w-4 text-gray-500 group-hover:text-black" /> |
||||
</template> |
||||
</a-input> |
||||
</div> |
||||
<NcButton type="ghost" size="small" class="!text-primary !bg-white" @click="toggleMarket"> |
||||
<div class="flex items-center gap-1 px-1 text-xs"> |
||||
<GeneralIcon icon="plus" /> |
||||
Add Extension |
||||
</div> |
||||
</NcButton> |
||||
</div> |
||||
<div class="flex items-center flex-col w-full nc-scrollbar-md"> |
||||
<ExtensionsWrapper v-for="ext in extensionList" :key="ext.id" :extension-id="ext.id" /> |
||||
</div> |
||||
</template> |
||||
<ExtensionsMarket v-if="isMarketVisible" v-model="isMarketVisible" /> |
||||
<ExtensionsDetails |
||||
v-if="isDetailsVisible && detailsExtensionId" |
||||
v-model="isDetailsVisible" |
||||
:extension-id="detailsExtensionId" |
||||
:from="detailsFrom" |
||||
/> |
||||
</Pane> |
||||
</template> |
||||
|
||||
<style lang="scss"></style> |
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts"> |
||||
interface Prop { |
||||
extensionId: string |
||||
} |
||||
|
||||
const { extensionId } = defineProps<Prop>() |
||||
</script> |
||||
|
||||
<template> |
||||
<NuxtErrorBoundary> |
||||
<ExtensionsExtension :extension-id="extensionId" /> |
||||
<template #error="{ error }"> |
||||
<ExtensionsExtension :extension-id="extensionId" :error="error" /> |
||||
</template> |
||||
</NuxtErrorBoundary> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"></style> |
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts"> |
||||
import { useCopy } from '~/composables/useCopy' |
||||
|
||||
const props = defineProps<{ |
||||
content?: string |
||||
timeout?: number |
||||
}>() |
||||
|
||||
const { copy } = useCopy() |
||||
const copied = ref(false) |
||||
|
||||
const copyContent = async () => { |
||||
await copy(props.content || '') |
||||
copied.value = true |
||||
setTimeout(() => { |
||||
copied.value = false |
||||
}, props.timeout || 2000) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<NcButton size="xsmall" type="text" @click="copyContent"> |
||||
<MdiCheck v-if="copied" class="h-3.5" /> |
||||
<component :is="iconMap.copy" v-else class="text-gray-800" /> |
||||
</NcButton> |
||||
</template> |