mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
This commit is contained in:
+4
-5
@@ -1,5 +1,4 @@
|
||||
VITE_APPWRITE_ENDPOINT=http://localhost/v1
|
||||
VITE_APPWRITE_GROWTH_ENDPOINT=
|
||||
VITE_GA_PROJECT=
|
||||
VITE_CONSOLE_MODE=self-hosted
|
||||
VITE_STRIPE_PUBLIC_KEY=
|
||||
PUBLIC_APPWRITE_ENDPOINT=https://localhost/v1
|
||||
PUBLIC_CONSOLE_MODE=self-hosted
|
||||
PUBLIC_STRIPE_KEY=
|
||||
PUBLIC_GROWTH_ENDPOINT=
|
||||
@@ -7,3 +7,4 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/playwright-report
|
||||
@@ -16,12 +16,14 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: E2E Tests
|
||||
run: npm run e2e
|
||||
run: pnpm run e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-cloud:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: appwrite/console-cloud
|
||||
tags: |
|
||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
|
||||
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
|
||||
publish-cloud-stage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: appwrite/console-cloud
|
||||
tags: |
|
||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}-stage
|
||||
type=semver,pattern={{major}}.{{minor}}-stage
|
||||
type=semver,pattern={{major}}-stage
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
|
||||
publish-self-hosted:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: appwrite/console
|
||||
tags: |
|
||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=self-hosted"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
- 'static/**/*'
|
||||
|
||||
env:
|
||||
VITE_APPWRITE_ENDPOINT: http://appwrite.test/v1
|
||||
PUBLIC_APPWRITE_ENDPOINT: http://appwrite.test/v1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -19,15 +19,17 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Audit dependencies
|
||||
run: npm audit --audit-level critical
|
||||
run: pnpm audit --audit-level high
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Svelte Diagnostics
|
||||
run: npm run check
|
||||
run: pnpm run check
|
||||
- name: Linter
|
||||
run: npm run lint
|
||||
run: pnpm run lint
|
||||
- name: Unit Tests
|
||||
run: npm run test
|
||||
run: pnpm run test
|
||||
- name: Build Console
|
||||
run: npm run build
|
||||
run: pnpm run build
|
||||
|
||||
@@ -10,6 +10,8 @@ node_modules
|
||||
!.env.example
|
||||
test-results/
|
||||
playwright-report/
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
.DS_STORE
|
||||
.cache
|
||||
@@ -148,3 +150,9 @@ dist
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
# IDE specifics
|
||||
.idea
|
||||
+13
-10
@@ -49,7 +49,7 @@ git clone https://github.com/appwrite/console.git appwrite-console
|
||||
Navigate to the Appwrite Console repository and install dependencies.
|
||||
|
||||
```bash
|
||||
cd appwrite-console && npm install
|
||||
cd appwrite-console && pnpm install
|
||||
```
|
||||
|
||||
### 3. Install and run Appwrite locally
|
||||
@@ -62,10 +62,13 @@ Follow the [install instructions](https://appwrite.io/docs/advanced/self-hosting
|
||||
|
||||
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
|
||||
|
||||
> **Note**
|
||||
> If you are updating from Appwrite `1.5.x`, be aware that the variables for the console in the `.env` / `.env.example` file have changed in `1.6.x`.
|
||||
|
||||
Finally, start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
> **Note**
|
||||
@@ -74,7 +77,7 @@ npm run dev
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.
|
||||
@@ -82,7 +85,7 @@ npm run build
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
This will run tests in the `tests/` directory.
|
||||
@@ -92,13 +95,13 @@ This will run tests in the `tests/` directory.
|
||||
Code should be consistently formatted everywhere. Before committing code, run the code-formatter.
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
pnpm run format
|
||||
```
|
||||
|
||||
### Linter
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
### Diagnostics
|
||||
@@ -110,7 +113,7 @@ Diagnostic tool that checks for the following:
|
||||
- TypeScript compiler errors
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
## Submit a Pull Request 🚀
|
||||
@@ -173,11 +176,11 @@ $ git push origin [name_of_your_new_branch]
|
||||
Before committing always make sure to run all available tools to improve the codebase:
|
||||
|
||||
- Formatter
|
||||
- `npm run format`
|
||||
- `pnpm run format`
|
||||
- Tests
|
||||
- `npm test`
|
||||
- `pnpm test`
|
||||
- Diagnostics
|
||||
- `npm run check`
|
||||
- `pnpm run check`
|
||||
|
||||
### Performance
|
||||
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
ADD ./package.json /app/package.json
|
||||
ADD ./pnpm-lock.yaml /app/pnpm-lock.yaml
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
|
||||
ADD ./build.js /app/build.js
|
||||
ADD ./tsconfig.json /app/tsconfig.json
|
||||
ADD ./svelte.config.js /app/svelte.config.js
|
||||
ADD ./vite.config.ts /app/vite.config.ts
|
||||
ADD ./src /app/src
|
||||
ADD ./static /app/static
|
||||
|
||||
ARG PUBLIC_CONSOLE_MODE
|
||||
ARG PUBLIC_APPWRITE_ENDPOINT
|
||||
ARG PUBLIC_GROWTH_ENDPOINT
|
||||
ARG PUBLIC_STRIPE_KEY
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
|
||||
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
|
||||
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
|
||||
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
|
||||
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
|
||||
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
|
||||
ENV NODE_OPTIONS=--max_old_space_size=8192
|
||||
|
||||
RUN pnpm run sync && pnpm run build
|
||||
|
||||
FROM nginx:1.25-alpine
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/build /usr/share/nginx/html/console
|
||||
@@ -4,7 +4,7 @@ import kleur from 'kleur';
|
||||
|
||||
const { bold, yellow } = kleur;
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const env = loadEnv('production', __dirname);
|
||||
const env = loadEnv('production', __dirname, 'PUBLIC_');
|
||||
|
||||
function log(text = '', prefix = '') {
|
||||
console.log(`${bold().green(`# ${prefix}`)}${text}`);
|
||||
@@ -23,9 +23,9 @@ async function main() {
|
||||
log();
|
||||
log(bold().magenta('APPWRITE CONSOLE'));
|
||||
log();
|
||||
logEnv('CONSOLE MODE', env?.VITE_CONSOLE_MODE);
|
||||
logEnv('APPWRITE ENDPOINT', env?.VITE_APPWRITE_ENDPOINT, 'relative');
|
||||
logEnv('GROWTH ENDPOINT', env?.VITE_APPWRITE_GROWTH_ENDPOINT);
|
||||
logEnv('CONSOLE MODE', env?.PUBLIC_CONSOLE_MODE);
|
||||
logEnv('APPWRITE ENDPOINT', env?.PUBLIC_APPWRITE_ENDPOINT, 'relative');
|
||||
logEnv('GROWTH ENDPOINT', env?.PUBLIC_GROWTH_ENDPOINT);
|
||||
log();
|
||||
logDelimiter();
|
||||
await build();
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
services:
|
||||
console:
|
||||
image: appwrite/console:dev
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
PUBLIC_CONSOLE_MODE: ${PUBLIC_CONSOLE_MODE}
|
||||
PUBLIC_APPWRITE_ENDPOINT: ${PUBLIC_APPWRITE_ENDPOINT}
|
||||
PUBLIC_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
|
||||
PUBLIC_STRIPE_KEY: ${PUBLIC_STRIPE_KEY}
|
||||
SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN}
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ./
|
||||
ignore:
|
||||
- .github
|
||||
- tests/
|
||||
- node_modules/
|
||||
- build/
|
||||
environment:
|
||||
- PUBLIC_CONSOLE_MODE
|
||||
- PUBLIC_APPWRITE_ENDPOINT
|
||||
- PUBLIC_GROWTH_ENDPOINT
|
||||
- PUBLIC_STRIPE_KEY
|
||||
ports:
|
||||
- '3000:80'
|
||||
@@ -0,0 +1,40 @@
|
||||
map $sent_http_content_type $expires {
|
||||
# cache everything for 1 year
|
||||
default 1y;
|
||||
# html files shouldn't be cached for single-page applications
|
||||
text/html off;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
# serve compressed file if filename.gz exists
|
||||
gzip_static on;
|
||||
|
||||
location /console {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri /console/index.html;
|
||||
|
||||
# Add cache headers
|
||||
expires $expires;
|
||||
add_header Pragma public;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
# Deny IE browsers from going into quirks mode
|
||||
add_header X-UA-Compatible "IE=Edge";
|
||||
# X-Frame-Options is to prevent from clickJacking attack
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
# This header enables the Cross-site scripting (XSS) filter
|
||||
add_header X-XSS-Protection "1; mode=block;";
|
||||
# disable content-type sniffing on some browsers.
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
}
|
||||
|
||||
location / {
|
||||
absolute_redirect off;
|
||||
return 301 /console;
|
||||
}
|
||||
}
|
||||
Generated
+1937
-1351
File diff suppressed because it is too large
Load Diff
+43
-43
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@appwrite/console",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
@@ -19,60 +19,60 @@
|
||||
"e2e:ui": "playwright test tests/e2e --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/console": "^1.2.0",
|
||||
"@appwrite.io/pink": "0.25.0",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
"@stripe/stripe-js": "^3.4.0",
|
||||
"ai": "^2.2.11",
|
||||
"analytics": "^0.8.9",
|
||||
"@sentry/sveltekit": "^8.26.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"ai": "^2.2.37",
|
||||
"analytics": "^0.8.14",
|
||||
"cron-parser": "^4.9.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"deep-equal": "^2.2.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"echarts": "^5.4.3",
|
||||
"nanoid": "^4.0.2",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"dayjs": "^1.11.12",
|
||||
"deep-equal": "^2.2.3",
|
||||
"echarts": "^5.5.1",
|
||||
"envfile": "^7.1.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"plausible-tracker": "^0.3.9",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"svelte-confetti": "^1.3.0",
|
||||
"svelte-confetti": "^1.4.0",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.1.4",
|
||||
"@melt-ui/svelte": "^0.61.2",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.3.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
"@testing-library/dom": "^9.0.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^4.0.3",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vitest/ui": "^1.2.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-svelte": "^2.33.0",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@sveltejs/adapter-static": "^3.0.4",
|
||||
"@sveltejs/kit": "^2.5.22",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/svelte": "^5.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.43.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"kleur": "^4.1.5",
|
||||
"prettier": "^3.2.2",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"sass": "^1.66.1",
|
||||
"svelte": "^4.2.9",
|
||||
"svelte-check": "^3.5.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"sass": "^1.77.8",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.5",
|
||||
"svelte-jester": "^2.3.2",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.11",
|
||||
"vitest": "^1.2.1"
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
|
||||
}
|
||||
|
||||
@@ -4,19 +4,20 @@ const config: PlaywrightTestConfig = {
|
||||
timeout: 120000,
|
||||
reportSlowTests: null,
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
retries: 3,
|
||||
retries: 1,
|
||||
use: {
|
||||
baseURL: 'http://localhost:4173/console/',
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
webServer: {
|
||||
timeout: 120000,
|
||||
env: {
|
||||
VITE_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
|
||||
VITE_CONSOLE_MODE: 'cloud',
|
||||
VITE_STRIPE_PUBLIC_KEY:
|
||||
PUBLIC_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
|
||||
PUBLIC_CONSOLE_MODE: 'cloud',
|
||||
PUBLIC_STRIPE_KEY:
|
||||
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
|
||||
},
|
||||
command: 'npm run build && npm run preview',
|
||||
command: 'pnpm run build && pnpm run preview',
|
||||
port: 4173
|
||||
}
|
||||
};
|
||||
|
||||
Generated
+7575
File diff suppressed because it is too large
Load Diff
+35
-14
@@ -2,44 +2,46 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Appwrite is an open-source platform for building applications at any scale, using your preferred programming languages and tools." />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/logos/appwrite-icon.svg" />
|
||||
<link rel="mask-icon" type="image/png" href="/logos/appwrite-icon.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/console/logos/appwrite-icon.svg" />
|
||||
<link rel="mask-icon" type="image/png" href="/console/logos/appwrite-icon.png" />
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/inter-v8-latin-600.woff2"
|
||||
href="/console/fonts/inter/inter-v8-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/inter-v8-latin-regular.woff2"
|
||||
href="/console/fonts/inter/inter-v8-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-500.woff2"
|
||||
href="/console/fonts/poppins/poppins-v19-latin-500.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-600.woff2"
|
||||
href="/console/fonts/poppins/poppins-v19-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/poppins/poppins-v19-latin-700.woff2"
|
||||
href="/console/fonts/poppins/poppins-v19-latin-700.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/source-code-pro/source-code-pro-v20-latin-regular.woff2"
|
||||
href="/console/fonts/source-code-pro/source-code-pro-v20-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
@@ -127,14 +129,13 @@
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link rel="preload" as="style" type="text/css" href="/fonts/main.css" />
|
||||
<link rel="stylesheet" href="/fonts/main.css" />
|
||||
<link rel="preload" as="style" type="text/css" href="/console/fonts/main.css" />
|
||||
<link rel="stylesheet" href="/console/css/loading.css" />
|
||||
<link rel="stylesheet" href="/console/fonts/main.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!-- {{CLOUD_OG}} -->
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<body data-sveltekit-preload-data="hover" data-loading="true">
|
||||
<script>
|
||||
let themeInUse = 'auto';
|
||||
const appwrite = localStorage.getItem('appwrite');
|
||||
@@ -155,5 +156,25 @@
|
||||
document.body.setAttribute('class', `theme-${themeInUse}`);
|
||||
</script>
|
||||
<div id="svelte">%sveltekit.body%</div>
|
||||
<div class="page-loader">
|
||||
<div class="animation">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<img
|
||||
src="/console/images/appwrite-logo-light.svg"
|
||||
width="120"
|
||||
height="22"
|
||||
class="logo-light"
|
||||
alt="Appwrite Logo" />
|
||||
<img
|
||||
src="/console/images/appwrite-logo-dark.svg"
|
||||
width="120"
|
||||
height="22"
|
||||
class="logo-dark"
|
||||
alt="Appwrite Logo" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+23
-10
@@ -1,14 +1,27 @@
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { AppwriteException } from '@appwrite.io/console';
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
import { isCloud, isProd } from '$lib/system';
|
||||
|
||||
export const handleError: HandleClientError = async ({ error, message, status }) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
status = error.code === 0 ? undefined : error.code;
|
||||
message = error.message;
|
||||
Sentry.init({
|
||||
enabled: isCloud && isProd,
|
||||
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
|
||||
tracesSampleRate: 1,
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 1,
|
||||
integrations: [Sentry.replayIntegration()]
|
||||
});
|
||||
|
||||
export const handleError: HandleClientError = Sentry.handleErrorWithSentry(
|
||||
async ({ error, message, status }) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
status = error.code === 0 ? undefined : error.code;
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
status
|
||||
};
|
||||
};
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { isCloud, isProd } from '$lib/system';
|
||||
|
||||
Sentry.init({
|
||||
enabled: isCloud && isProd,
|
||||
dsn: 'https://c7ce178bdedd486480317b72f282fd39@o1063647.ingest.us.sentry.io/4504158071422976',
|
||||
tracesSampleRate: 1.0
|
||||
});
|
||||
|
||||
export const handle = sequence(sentryHandle());
|
||||
|
||||
export const handleError = handleErrorWithSentry();
|
||||
@@ -194,6 +194,8 @@ export enum Submit {
|
||||
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
|
||||
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
|
||||
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
|
||||
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
|
||||
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
|
||||
SessionsLengthUpdate = 'submit_sessions_length_update',
|
||||
SessionsLimitUpdate = 'submit_sessions_limit_update',
|
||||
SessionDelete = 'submit_session_delete',
|
||||
@@ -226,13 +228,16 @@ export enum Submit {
|
||||
FunctionUpdateLogging = 'submit_function_update_logging',
|
||||
FunctionUpdateTimeout = 'submit_function_update_timeout',
|
||||
FunctionUpdateEvents = 'submit_function_update_events',
|
||||
FunctionUpdateScopes = 'submit_function_key_update_scopes',
|
||||
FunctionConnectRepo = 'submit_function_connect_repo',
|
||||
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
|
||||
FunctionRedeploy = 'submit_function_redeploy',
|
||||
DeploymentCreate = 'submit_deployment_create',
|
||||
DeploymentDelete = 'submit_deployment_delete',
|
||||
DeploymentUpdate = 'submit_deployment_update',
|
||||
DeploymentCancel = 'submit_deployment_cancel',
|
||||
ExecutionCreate = 'submit_execution_create',
|
||||
ExecutionDelete = 'submit_execution_delete',
|
||||
VariableCreate = 'submit_variable_create',
|
||||
VariableDelete = 'submit_variable_delete',
|
||||
VariableUpdate = 'submit_variable_update',
|
||||
@@ -314,5 +319,6 @@ export enum Submit {
|
||||
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
|
||||
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
|
||||
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete',
|
||||
ApplyQuickFilter = 'submit_apply_quick_filter'
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
|
||||
import { isLanguage, type Language } from '$lib/components/code.svelte';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { VARS } from '$lib/system';
|
||||
|
||||
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`;
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
|
||||
const endpoint = getApiEndpoint();
|
||||
const { input, handleSubmit, completion, isLoading, complete, error } = useCompletion({
|
||||
api: endpoint + '/console/assistant',
|
||||
headers: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { initCreateAttribute } from '$routes/console/project-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
|
||||
import { attributeOptions } from '$routes/console/project-[project]/databases/database-[database]/collection-[collection]/attributes/store';
|
||||
import { initCreateAttribute } from '$routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/+layout.svelte';
|
||||
import { attributeOptions } from '$routes/(console)/project-[project]/databases/database-[database]/collection-[collection]/attributes/store';
|
||||
import Template from './template.svelte';
|
||||
|
||||
let search = '';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { providers } from '$routes/console/project-[project]/messaging/providers/store';
|
||||
import { providers } from '$routes/(console)/project-[project]/messaging/providers/store';
|
||||
import {
|
||||
messageParams,
|
||||
providerType,
|
||||
targetsById
|
||||
} from '$routes/console/project-[project]/messaging/wizard/store';
|
||||
} from '$routes/(console)/project-[project]/messaging/wizard/store';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import Template from './template.svelte';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Create from '$routes/console/project-[project]/messaging/create.svelte';
|
||||
import { topicsById } from '$routes/console/project-[project]/messaging/store';
|
||||
import Create from '$routes/(console)/project-[project]/messaging/create.svelte';
|
||||
import { topicsById } from '$routes/(console)/project-[project]/messaging/store';
|
||||
|
||||
let search = '';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {
|
||||
Platform,
|
||||
addPlatform
|
||||
} from '$routes/console/project-[project]/overview/platforms/+page.svelte';
|
||||
} from '$routes/(console)/project-[project]/overview/platforms/+page.svelte';
|
||||
import Template from './template.svelte';
|
||||
|
||||
let search = '';
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Command, Searcher } from '../commands';
|
||||
import { addSubPanel } from '../subPanels';
|
||||
import { FilesPanel } from '../panels';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
const getBucketCommand = (bucket: Models.Bucket, projectId: string) => {
|
||||
return {
|
||||
label: `${bucket.name}`,
|
||||
callback() {
|
||||
goto(`/console/project-${projectId}/storage/bucket-${bucket.$id}`);
|
||||
goto(`${base}/project-${projectId}/storage/bucket-${bucket.$id}`);
|
||||
},
|
||||
group: 'buckets',
|
||||
icon: 'folder'
|
||||
@@ -31,7 +32,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
{
|
||||
label: 'Find files',
|
||||
async callback() {
|
||||
await goto(`/console/project-${$project.$id}/storage/bucket-${bucket.$id}`);
|
||||
await goto(`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}`);
|
||||
addSubPanel(FilesPanel);
|
||||
},
|
||||
group: 'buckets',
|
||||
@@ -43,7 +44,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
label: 'Permissions',
|
||||
async callback() {
|
||||
await goto(
|
||||
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#permissions`
|
||||
`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#permissions`
|
||||
);
|
||||
scrollBy({ top: -100 });
|
||||
},
|
||||
@@ -55,7 +56,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
label: 'Extensions',
|
||||
async callback() {
|
||||
await goto(
|
||||
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#extensions`
|
||||
`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#extensions`
|
||||
);
|
||||
},
|
||||
group: 'buckets',
|
||||
@@ -66,7 +67,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
label: 'File Security',
|
||||
async callback() {
|
||||
await goto(
|
||||
`/console/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#file-security`
|
||||
`${base}/project-${$project.$id}/storage/bucket-${bucket.$id}/settings#file-security`
|
||||
);
|
||||
scrollBy({ top: -100 });
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { database } from '$routes/console/project-[project]/databases/database-[database]/store';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { database } from '$routes/(console)/project-[project]/databases/database-[database]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const collectionsSearcher = (async (query: string) => {
|
||||
const databaseId = get(database).$id;
|
||||
@@ -19,7 +20,7 @@ export const collectionsSearcher = (async (query: string) => {
|
||||
label: col.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`/console/project-${projectId}/databases/database-${databaseId}/collection-${col.$id}`
|
||||
`${base}/project-${projectId}/databases/database-${databaseId}/collection-${col.$id}`
|
||||
);
|
||||
}
|
||||
}) as const
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const dbSearcher = (async (query: string) => {
|
||||
const { databases } = await sdk.forProject.databases.list();
|
||||
@@ -15,7 +16,7 @@ export const dbSearcher = (async (query: string) => {
|
||||
group: 'databases',
|
||||
label: db.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${get(project).$id}/databases/database-${db.$id}`);
|
||||
goto(`${base}/project-${get(project).$id}/databases/database-${db.$id}`);
|
||||
},
|
||||
icon: 'database'
|
||||
}) as const
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { bucket } from '$routes/console/project-[project]/storage/bucket-[bucket]/store';
|
||||
import { bucket } from '$routes/(console)/project-[project]/storage/bucket-[bucket]/store';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const fileSearcher = (async (query: string) => {
|
||||
const $bucket = get(bucket);
|
||||
@@ -19,7 +20,7 @@ export const fileSearcher = (async (query: string) => {
|
||||
return files.map((file) => ({
|
||||
label: file.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${$project.$id}/storage/bucket-${$bucket.$id}/file-${file.$id}`);
|
||||
goto(`${base}/project-${$project.$id}/storage/bucket-${$bucket.$id}/file-${file.$id}`);
|
||||
},
|
||||
icon: 'document',
|
||||
group: 'files'
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/stores';
|
||||
import { showCreateDeployment } from '$routes/console/project-[project]/functions/function-[function]/store';
|
||||
import { showCreateDeployment } from '$routes/(console)/project-[project]/functions/function-[function]/store';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
const getFunctionCommand = (fn: Models.Function, projectId: string) => {
|
||||
return {
|
||||
label: fn.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/functions/function-${fn.$id}`);
|
||||
goto(`${base}/project-${projectId}/functions/function-${fn.$id}`);
|
||||
},
|
||||
group: 'functions',
|
||||
icon: 'lightning-bolt'
|
||||
@@ -35,7 +36,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
async callback() {
|
||||
const $page = get(page);
|
||||
if (!$page.url.pathname.endsWith(func.$id)) {
|
||||
await goto(`/console/project-${projectId}/functions/function-${func.$id}`);
|
||||
await goto(`${base}/project-${projectId}/functions/function-${func.$id}`);
|
||||
}
|
||||
showCreateDeployment.set(true);
|
||||
},
|
||||
@@ -46,7 +47,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
label: 'Go to deployments',
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(`/console/project-${projectId}/functions/function-${func.$id}`);
|
||||
goto(`${base}/project-${projectId}/functions/function-${func.$id}`);
|
||||
},
|
||||
group: 'functions'
|
||||
},
|
||||
@@ -54,7 +55,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
label: 'Go to usage',
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(`/console/project-${projectId}/functions/function-${func.$id}/usage`);
|
||||
goto(`${base}/project-${projectId}/functions/function-${func.$id}/usage`);
|
||||
},
|
||||
group: 'functions'
|
||||
},
|
||||
@@ -62,7 +63,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
label: 'Go to executions',
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(`/console/project-${projectId}/functions/function-${func.$id}/executions`);
|
||||
goto(`${base}/project-${projectId}/functions/function-${func.$id}/executions`);
|
||||
},
|
||||
group: 'functions'
|
||||
},
|
||||
@@ -70,7 +71,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
label: 'Go to settings',
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(`/console/project-${projectId}/functions/function-${func.$id}/settings`);
|
||||
goto(`${base}/project-${projectId}/functions/function-${func.$id}/settings`);
|
||||
},
|
||||
group: 'functions'
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import { type Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
const getLabel = (message) => {
|
||||
switch (message.providerType) {
|
||||
@@ -44,7 +45,7 @@ export const messagesSearcher = (async (query: string) => {
|
||||
group: 'messages',
|
||||
label: getLabel(message),
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/messaging/message-${message.$id}`);
|
||||
goto(`${base}/project-${projectId}/messaging/message-${message.$id}`);
|
||||
},
|
||||
icon: getIcon(message)
|
||||
}) as const
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Searcher } from '../commands';
|
||||
|
||||
@@ -10,7 +11,7 @@ export const orgSearcher = (async (query: string) => {
|
||||
return {
|
||||
label: organization.name,
|
||||
callback: () => {
|
||||
goto(`/console/organization-${organization.$id}`);
|
||||
goto(`${base}/organization-${organization.$id}`);
|
||||
},
|
||||
group: 'organizations'
|
||||
} as const;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const projectsSearcher = (async (query: string) => {
|
||||
const { projects } = await sdk.forConsole.projects.list([
|
||||
@@ -17,7 +18,7 @@ export const projectsSearcher = (async (query: string) => {
|
||||
return {
|
||||
label: project.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${project.$id}`);
|
||||
goto(`${base}/project-${project.$id}`);
|
||||
},
|
||||
group: 'projects'
|
||||
} as const;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getProviderDisplayNameAndIcon } from '$routes/console/project-[project]/messaging/provider.svelte';
|
||||
import { getProviderDisplayNameAndIcon } from '$routes/(console)/project-[project]/messaging/provider.svelte';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
const getIcon = (provider: string) => {
|
||||
const { icon } = getProviderDisplayNameAndIcon(provider);
|
||||
@@ -24,7 +25,7 @@ export const providersSearcher = (async (query: string) => {
|
||||
label: provider.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`/console/project-${projectId}/messaging/providers/provider-${provider.$id}`
|
||||
`${base}/project-${projectId}/messaging/providers/provider-${provider.$id}`
|
||||
);
|
||||
},
|
||||
image: getIcon(provider.provider)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Command, Searcher } from '../commands';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
const getTeamCommand = (team: Models.Team<Models.Preferences>, projectId: string) =>
|
||||
({
|
||||
label: team.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/auth/teams/team-${team.$id}`);
|
||||
goto(`${base}/project-${projectId}/auth/teams/team-${team.$id}`);
|
||||
},
|
||||
group: 'teams',
|
||||
icon: 'user-circle'
|
||||
@@ -25,7 +26,7 @@ export const teamSearcher = (async (query: string) => {
|
||||
{
|
||||
label: 'Go to members',
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/auth/teams/team-${teams[0].$id}/members`);
|
||||
goto(`${base}/project-${projectId}/auth/teams/team-${teams[0].$id}/members`);
|
||||
},
|
||||
group: 'teams',
|
||||
nested: true
|
||||
@@ -34,7 +35,7 @@ export const teamSearcher = (async (query: string) => {
|
||||
{
|
||||
label: 'Go to activity',
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/auth/teams/team-${teams[0].$id}/activity`);
|
||||
goto(`${base}/project-${projectId}/auth/teams/team-${teams[0].$id}/activity`);
|
||||
},
|
||||
group: 'teams',
|
||||
nested: true
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const topicsSearcher = (async (query: string) => {
|
||||
const { topics } = await sdk.forProject.messaging.listTopics([], query || undefined);
|
||||
@@ -17,7 +18,7 @@ export const topicsSearcher = (async (query: string) => {
|
||||
group: 'topics',
|
||||
label: topic.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/messaging/topics/topic-${topic.$id}`);
|
||||
goto(`${base}/project-${projectId}/messaging/topics/topic-${topic.$id}`);
|
||||
},
|
||||
icon: 'send'
|
||||
}) as const
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Command, Searcher } from '../commands';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { promptDeleteUser } from '$routes/console/project-[project]/auth/user-[user]/dangerZone.svelte';
|
||||
import { promptDeleteUser } from '$routes/(console)/project-[project]/auth/user-[user]/dangerZone.svelte';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
const getUserCommand = (user: Models.User<Models.Preferences>, projectId: string) =>
|
||||
({
|
||||
label: user.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/auth/user-${user.$id}`);
|
||||
goto(`${base}/project-${projectId}/auth/user-${user.$id}`);
|
||||
},
|
||||
group: 'users',
|
||||
icon: 'user-circle'
|
||||
@@ -35,7 +36,7 @@ export const userSearcher = (async (query: string) => {
|
||||
{
|
||||
label: 'Go to activity',
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/activity`);
|
||||
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/activity`);
|
||||
},
|
||||
group: 'users',
|
||||
nested: true
|
||||
@@ -43,7 +44,7 @@ export const userSearcher = (async (query: string) => {
|
||||
{
|
||||
label: 'Go to sessions',
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/sessions`);
|
||||
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/sessions`);
|
||||
},
|
||||
group: 'users',
|
||||
nested: true
|
||||
@@ -51,7 +52,7 @@ export const userSearcher = (async (query: string) => {
|
||||
{
|
||||
label: 'Go to memberships',
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/memberships`);
|
||||
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/memberships`);
|
||||
},
|
||||
group: 'users',
|
||||
nested: true
|
||||
|
||||
@@ -20,10 +20,7 @@
|
||||
plan. Consider upgrading to increase your resource usage.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
href={`${base}/console/organization-${$organization.$id}/usage`}
|
||||
text
|
||||
fullWidthMobile>
|
||||
<Button href={`${base}/organization-${$organization.$id}/usage`} text fullWidthMobile>
|
||||
<span class="text">View usage</span>
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
import { orgMissingPaymentMethod } from '$routes/console/store';
|
||||
import { orgMissingPaymentMethod } from '$routes/(console)/store';
|
||||
</script>
|
||||
|
||||
{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
@@ -16,9 +17,7 @@
|
||||
your projects.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
secondary
|
||||
href={`/console/organization-${$orgMissingPaymentMethod.$id}/billing`}>
|
||||
<Button secondary href={`${base}/organization-${$orgMissingPaymentMethod.$id}/billing`}>
|
||||
Add payment method
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { activeHeaderAlert } from '$routes/console/store';
|
||||
import { activeHeaderAlert } from '$routes/(console)/store';
|
||||
import GradientBanner from '../gradientBanner.svelte';
|
||||
|
||||
let show = true;
|
||||
@@ -29,7 +29,7 @@
|
||||
secondary
|
||||
fullWidthMobile
|
||||
class="u-line-height-1"
|
||||
href={`${base}/console/apply-credit?code=appw50&org=${$organization.$id}`}
|
||||
href={`${base}/apply-credit?code=appw50&org=${$organization.$id}`}
|
||||
on:click={() => {
|
||||
trackEvent('click_credits_redeem', {
|
||||
from: 'button',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { actionRequiredInvoices, hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { VARS } from '$lib/system';
|
||||
|
||||
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${$page.url.origin}/v1`;
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
const endpoint = getApiEndpoint();
|
||||
</script>
|
||||
|
||||
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
@@ -21,7 +21,7 @@
|
||||
</Button>
|
||||
<Button
|
||||
secondary
|
||||
href={`/console/organization-${$organization.$id}/billing?type=confirmation&invoice=${$actionRequiredInvoices.invoices[0].$id}`}>
|
||||
href={`${base}/organization-${$organization.$id}/billing?type=confirmation&invoice=${$actionRequiredInvoices.invoices[0].$id}`}>
|
||||
Authorize payment
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
href={`${base}/console/organization-${$failedInvoice?.teamId}/billing#paymentMethods`}
|
||||
href={`${base}/organization-${$failedInvoice?.teamId}/billing#paymentMethods`}
|
||||
secondary
|
||||
fullWidthMobile>
|
||||
<span class="text">Update billing details</span>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let couponData: Partial<Coupon>;
|
||||
export let billingBudget: number;
|
||||
export let fixedCoupon = false; // If true, the coupon cannot be removed
|
||||
export let isDowngrade = false;
|
||||
|
||||
const today = new Date();
|
||||
const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
@@ -41,8 +42,8 @@
|
||||
<p class="text">{formatCurrency(currentPlan.price)}</p>
|
||||
</span>
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">Additional seats ({collaborators?.length})</p>
|
||||
<p class="text">
|
||||
<p class="text" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
|
||||
<p class="text" class:u-bold={isDowngrade}>
|
||||
{formatCurrency(extraSeatsCost)}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -7,10 +8,18 @@
|
||||
<div class="top-banner">
|
||||
<div class="top-banner-bg">
|
||||
<div class="top-banner-bg-1">
|
||||
<img src="/images/top-banner/bg-pink-desktop.svg" width="1283" height="1278" alt="" />
|
||||
<img
|
||||
src={`${base}/images/top-banner/bg-pink-desktop.svg`}
|
||||
width="1283"
|
||||
height="1278"
|
||||
alt="" />
|
||||
</div>
|
||||
<div class="top-banner-bg-2">
|
||||
<img src="/images/top-banner/bg-mint-desktop.svg" width="1051" height="1271" alt="" />
|
||||
<img
|
||||
src={`${base}/images/top-banner/bg-mint-desktop.svg`}
|
||||
width="1051"
|
||||
height="1271"
|
||||
alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-banner-content u-color-text-primary">
|
||||
|
||||
@@ -6,3 +6,4 @@ export { default as EstimatedTotalBox } from './estimatedTotalBox.svelte';
|
||||
export { default as PlanComparisonBox } from './planComparisonBox.svelte';
|
||||
export { default as EmptyCardCloud } from './emptyCardCloud.svelte';
|
||||
export { default as CreditsApplied } from './creditsApplied.svelte';
|
||||
export { default as PlanSelection } from './planSelection.svelte';
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { plansInfo, tierFree, tierPro, type Tier } from '$lib/stores/billing';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
|
||||
let selectedTab: 'tier-0' | 'tier-1' = 'tier-0';
|
||||
let selectedTab: Tier = BillingPlan.FREE;
|
||||
export let downgrade = false;
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === 'tier-0'}
|
||||
on:click={() => (selectedTab = 'tier-0')}>
|
||||
Free
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === 'tier-1'}
|
||||
on:click={() => (selectedTab = 'tier-1')}>
|
||||
Pro
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
<Card style="--card-padding: 1.5rem">
|
||||
<div class="comparison-box">
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.FREE}
|
||||
on:click={() => (selectedTab = BillingPlan.FREE)}>
|
||||
{tierFree.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.PRO}
|
||||
on:click={() => (selectedTab = BillingPlan.PRO)}>
|
||||
{tierPro.name}
|
||||
</SecondaryTabsItem>
|
||||
<!-- <SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.SCALE}
|
||||
on:click={() => (selectedTab = BillingPlan.SCALE)}>
|
||||
{tierScale.name}
|
||||
</SecondaryTabsItem> -->
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
{#if selectedTab === 'tier-0'}
|
||||
{#if selectedTab === BillingPlan.FREE}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
@@ -86,16 +94,50 @@
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if selectedTab === 'tier-1'}
|
||||
{:else if selectedTab === BillingPlan.PRO}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
<li>Everything in the Free plan, plus:</li>
|
||||
<p class="u-margin-block-start-8">Everything in the Free plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{:else if selectedTab === BillingPlan.SCALE}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles <span class="inline-tag">Coming soon</span></li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
<li>SSO <span class="inline-tag">Coming soon</span></li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.comparison-box {
|
||||
border-radius: var(--border-radius-small);
|
||||
background: hsl(var(--color-neutral-5));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box {
|
||||
background: hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-0));
|
||||
border: 1px solid hsl(var(--color-neutral-10));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-80));
|
||||
border: 1px solid hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.inline-tag {
|
||||
line-height: 140%;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
export let tier: Tier;
|
||||
export let members: number;
|
||||
|
||||
const plan = $plansInfo?.get(tier);
|
||||
let excess: {
|
||||
@@ -41,7 +42,7 @@
|
||||
$organization.billingCurrentInvoiceDate,
|
||||
new Date().toISOString()
|
||||
);
|
||||
excess = calculateExcess(usage, plan, $organization);
|
||||
excess = calculateExcess(usage, plan, members);
|
||||
showExcess = Object.values(excess).some((value) => value > 0);
|
||||
});
|
||||
</script>
|
||||
@@ -98,7 +99,8 @@
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
{humanFileSize(excess?.storage)}
|
||||
{humanFileSize(excess?.storage).value}
|
||||
{humanFileSize(excess?.storage).unit}
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { LabelCard } from '..';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let anyOrgFree = false;
|
||||
export let isNewOrg = false;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
|
||||
</script>
|
||||
|
||||
{#if billingPlan}
|
||||
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={anyOrgFree}
|
||||
value={BillingPlan.FREE}
|
||||
tooltipShow={anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account."
|
||||
padding={1.5}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierFree.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<LabelCard name="plan" bind:group={billingPlan} value={BillingPlan.PRO} padding={1.5}>
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE}
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
value={BillingPlan.SCALE}
|
||||
padding={1.5}>
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierScale.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierScale.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -53,7 +53,7 @@
|
||||
{style}
|
||||
on:click
|
||||
on:keyup={clickOnEnter}
|
||||
role={href || isButton ? 'button' : 'generic'}
|
||||
role={href || isButton ? 'button' : 'presentation'}
|
||||
{href}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
bind:this={tooltip}
|
||||
class:u-width-full-line={fullWidth}
|
||||
style:--arrow-size={`${arrowSize}px`}
|
||||
style:z-index="10">
|
||||
style:z-index="21">
|
||||
<div
|
||||
class="drop-arrow"
|
||||
class:is-popover={isPopover}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
export let wrapperFullWidth = false;
|
||||
export let position: 'relative' | 'static' = 'relative';
|
||||
export let noMaxWidthList = false;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
<Drop
|
||||
@@ -29,17 +31,16 @@
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="drop is-no-arrow"
|
||||
class="drop is-no-arrow {classes}"
|
||||
class:u-max-width-100-percent={fullWidth}
|
||||
style={`${width ? `--drop-width-size-desktop:${width}rem; ` : ''} ${
|
||||
position === 'static' ? 'position:static' : 'position:relative'
|
||||
}`}>
|
||||
style:--drop-width-size-desktop={width ? `${width}rem` : ''}
|
||||
style:position>
|
||||
{#if $$slots.list}
|
||||
<section
|
||||
class:u-max-width-none={noMaxWidthList}
|
||||
class:u-overflow-y-auto={scrollable}
|
||||
class:u-max-height-200={scrollable}
|
||||
class="drop-section">
|
||||
class="drop-section"
|
||||
style={noMaxWidthList ? 'max-inline-size: 100%' : ''}>
|
||||
<ul class="drop-list">
|
||||
<slot name="list" />
|
||||
</ul>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let icon: string = null;
|
||||
export let event: string = null;
|
||||
export let loading = false;
|
||||
export let padding: number | null = null;
|
||||
|
||||
function track() {
|
||||
if (!event) {
|
||||
@@ -20,6 +21,8 @@
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button u-flex u-cross-center u-main-space-between"
|
||||
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
|
||||
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
|
||||
on:click={track}
|
||||
on:click|preventDefault
|
||||
{disabled}>
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
on:click={track}
|
||||
aria-label="create {target}">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img src={EmptyDark} alt="create" aria-hidden="true" width="376" />
|
||||
<img src={EmptyDark} alt="create" aria-hidden="true" height="242" />
|
||||
{:else}
|
||||
<img src={EmptyLight} alt="create" aria-hidden="true" width="376" />
|
||||
<img src={EmptyLight} alt="create" aria-hidden="true" height="242" />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script>
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { getNextTier, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { getNextTier, tierToPlan } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import Card from './card.svelte';
|
||||
|
||||
@@ -21,22 +19,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="u-stretch u-flex-vertical">
|
||||
<div class="u-stretch u-flex-vertical u-main-center">
|
||||
<h3 class="body-text-2 u-bold"><slot name="title" /></h3>
|
||||
<p class="u-margin-block-start-8">
|
||||
<slot nextTier={tierToPlan(getNextTier($organization.billingPlan)).name} />
|
||||
</p>
|
||||
<Button
|
||||
class="u-margin-block-start-32"
|
||||
secondary
|
||||
fullWidth
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source
|
||||
});
|
||||
}}>Upgrade plan</Button>
|
||||
<slot name="cta" {source} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { EmptySearch } from '.';
|
||||
import { queries } from './filters';
|
||||
|
||||
export let resource;
|
||||
export let resource: string;
|
||||
</script>
|
||||
|
||||
<EmptySearch hidePages>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<article class="card u-grid u-cross-center u-width-full-line common-section">
|
||||
<div class="u-flex u-flex-vertical u-cross-center u-gap-24 u-overflow-hidden">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img src={Dark} alt="create" aria-hidden="true" />
|
||||
<img src={Dark} height="175" alt="create" aria-hidden="true" />
|
||||
{:else}
|
||||
<img src={Light} alt="create" aria-hidden="true" />
|
||||
<img src={Light} height="175" alt="create" aria-hidden="true" />
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
<Button
|
||||
secondary
|
||||
external
|
||||
href={`${base}/console/project-${$page.params.project}/storage`}>
|
||||
href={`${base}/project-${$page.params.project}/storage`}>
|
||||
Create bucket
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
InputText,
|
||||
InputTags,
|
||||
FormList,
|
||||
InputSelectCheckbox
|
||||
InputSelectCheckbox,
|
||||
InputDateTime
|
||||
} from '$lib/elements/forms';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { tags, queries, type TagValue, operators, addFilter } from './store';
|
||||
import { tags, operators, addFilter, queries } from './store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { TagList } from '.';
|
||||
|
||||
// We cast to any to not cause type errors in the input components
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
@@ -21,6 +22,7 @@
|
||||
export let columnId: string | null = null;
|
||||
export let arrayValues: string[] = [];
|
||||
export let operatorKey: string | null = null;
|
||||
export let singleCondition = false;
|
||||
|
||||
$: column = $columns.find((c) => c.id === columnId) as Column;
|
||||
|
||||
@@ -41,6 +43,10 @@
|
||||
|
||||
onMount(() => {
|
||||
value = column?.array ? [] : null;
|
||||
if (column?.type === 'datetime') {
|
||||
const today = new Date();
|
||||
value = today.toISOString();
|
||||
}
|
||||
});
|
||||
|
||||
function addFilterAndReset() {
|
||||
@@ -49,21 +55,9 @@
|
||||
operatorKey = null;
|
||||
value = null;
|
||||
arrayValues = [];
|
||||
}
|
||||
|
||||
function tagFormat(node: HTMLElement) {
|
||||
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
}
|
||||
|
||||
function isTypeTagValue(obj: any): obj is TagValue {
|
||||
if (typeof obj === 'string') return false;
|
||||
return (
|
||||
obj &&
|
||||
typeof obj.tag === 'string' &&
|
||||
(typeof obj.value === 'string' ||
|
||||
typeof obj.value === 'number' ||
|
||||
Array.isArray(obj.value))
|
||||
);
|
||||
if (singleCondition) {
|
||||
queries.apply();
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
@@ -75,7 +69,7 @@
|
||||
|
||||
<div>
|
||||
<form on:submit|preventDefault={addFilterAndReset}>
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16 u-flex-vertical-mobile">
|
||||
<InputSelect
|
||||
id="column"
|
||||
options={$columns
|
||||
@@ -101,10 +95,10 @@
|
||||
name="value"
|
||||
bind:tags={arrayValues}
|
||||
placeholder="Select value"
|
||||
options={column?.elements?.map((value) => ({
|
||||
label: value,
|
||||
value,
|
||||
checked: arrayValues.includes(value)
|
||||
options={column?.elements?.map((e) => ({
|
||||
label: e?.label ?? e,
|
||||
value: e?.value ?? e,
|
||||
checked: arrayValues.includes(e?.value ?? e)
|
||||
}))}>
|
||||
</InputSelectCheckbox>
|
||||
{:else}
|
||||
@@ -123,7 +117,10 @@
|
||||
id="value"
|
||||
bind:value
|
||||
placeholder="Select value"
|
||||
options={column?.elements?.map((value) => ({ label: value, value }))}
|
||||
options={column?.elements?.map((e) => ({
|
||||
label: e?.label ?? e,
|
||||
value: e?.value ?? e
|
||||
}))}
|
||||
label="Value"
|
||||
showLabel={false} />
|
||||
{:else if column.type === 'integer' || column.type === 'double'}
|
||||
@@ -138,48 +135,34 @@
|
||||
{ label: 'False', value: false }
|
||||
].filter(Boolean)}
|
||||
bind:value />
|
||||
{:else if column.type === 'datetime'}
|
||||
{#key value}
|
||||
<InputDateTime
|
||||
id="value"
|
||||
bind:value
|
||||
label="value"
|
||||
showLabel={false}
|
||||
step={60} />
|
||||
{/key}
|
||||
{:else}
|
||||
<InputText id="value" bind:value placeholder="Enter value" />
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button text disabled={isDisabled} class="u-margin-block-start-4" noMargin submit>
|
||||
<i class="icon-plus" />
|
||||
Add condition
|
||||
</Button>
|
||||
{#if !singleCondition}
|
||||
<Button text disabled={isDisabled} class="u-margin-block-start-4" noMargin submit>
|
||||
<i class="icon-plus" />
|
||||
Add condition
|
||||
</Button>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
|
||||
{#each $tags as tag (tag)}
|
||||
{#if isTypeTagValue(tag)}
|
||||
<button
|
||||
use:tooltip={{
|
||||
content: tag?.value?.toString()
|
||||
}}
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag.tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{#if !singleCondition}
|
||||
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
|
||||
<TagList />
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -5,13 +5,27 @@
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import Content from './content.svelte';
|
||||
import { addFilter, queries, queriesAreDirty, queryParamToMap, tags } from './store';
|
||||
import {
|
||||
addFilter,
|
||||
queries,
|
||||
queriesAreDirty,
|
||||
queryParamToMap,
|
||||
tags,
|
||||
ValidOperators
|
||||
} from './store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let query = '[]';
|
||||
export let columns: Writable<Column[]>;
|
||||
export let disabled = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let singleCondition = false;
|
||||
export let clearOnClick = false; // When enabled the user doesn't have to click apply to clear the filters
|
||||
export let enableApply = false;
|
||||
export let quickFilters = false;
|
||||
let displayQuickFilters = quickFilters;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const parsedQueries = queryParamToMap(query);
|
||||
queries.set(parsedQueries);
|
||||
|
||||
@@ -36,10 +50,22 @@
|
||||
function clearAll() {
|
||||
selectedColumn = null;
|
||||
queries.clearAll();
|
||||
if (clearOnClick) {
|
||||
queries.apply();
|
||||
}
|
||||
}
|
||||
|
||||
function apply() {
|
||||
if (selectedColumn && operatorKey && value) {
|
||||
if (quickFilters && displayQuickFilters) {
|
||||
dispatch('apply');
|
||||
} else if (
|
||||
selectedColumn &&
|
||||
operatorKey &&
|
||||
(operatorKey === ValidOperators.IsNotNull ||
|
||||
operatorKey === ValidOperators.IsNull ||
|
||||
value ||
|
||||
arrayValues.length)
|
||||
) {
|
||||
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
|
||||
selectedColumn = null;
|
||||
value = null;
|
||||
@@ -49,16 +75,106 @@
|
||||
queries.apply();
|
||||
}
|
||||
|
||||
$: if (!showFiltersDesktop && !showFiltersMobile) {
|
||||
selectedColumn = null;
|
||||
function afterApply(
|
||||
e: CustomEvent<{
|
||||
applied: number;
|
||||
}>
|
||||
) {
|
||||
applied = e.detail.applied;
|
||||
if (singleCondition) {
|
||||
showFiltersDesktop = false;
|
||||
showFiltersMobile = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: isButtonDisabled = $queriesAreDirty ? false : !selectedColumn || !operatorKey || !value;
|
||||
$: if (!showFiltersDesktop && !showFiltersMobile) {
|
||||
selectedColumn = null;
|
||||
value = null;
|
||||
operatorKey = null;
|
||||
arrayValues = [];
|
||||
}
|
||||
|
||||
$: isButtonDisabled =
|
||||
$queriesAreDirty || (quickFilters && displayQuickFilters && enableApply)
|
||||
? false
|
||||
: !selectedColumn ||
|
||||
!operatorKey ||
|
||||
(!value &&
|
||||
!arrayValues.length &&
|
||||
operatorKey !== ValidOperators.IsNotNull &&
|
||||
operatorKey !== ValidOperators.IsNull);
|
||||
|
||||
function toggleDropdown() {
|
||||
dispatch('toggle', { show: !showFiltersDesktop });
|
||||
showFiltersDesktop = !showFiltersDesktop;
|
||||
}
|
||||
function toggleMobileModal() {
|
||||
dispatch('toggle', { show: !showFiltersMobile });
|
||||
showFiltersMobile = !showFiltersMobile;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="is-not-mobile">
|
||||
<Drop bind:show={showFiltersDesktop} noArrow>
|
||||
<Button secondary on:click={() => (showFiltersDesktop = !showFiltersDesktop)} {disabled}>
|
||||
<slot {disabled} toggle={toggleDropdown}>
|
||||
<Button secondary on:click={toggleDropdown} {disabled}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
<span class="inline-tag">
|
||||
{applied}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</slot>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card">
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<hr />
|
||||
<div
|
||||
class="u-flex u-cross-center u-margin-block-start-16"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button text on:click={toggleDropdown}>Cancel</Button>
|
||||
{:else}
|
||||
<Button disabled={applied === 0} text on:click={clearAll}>
|
||||
Clear all
|
||||
</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
</div>
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<slot name="mobile" {disabled} toggle={toggleMobileModal}>
|
||||
<Button secondary on:click={toggleMobileModal} {fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
@@ -67,54 +183,49 @@
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card">
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{columns}
|
||||
on:apply={(e) => (applied = e.detail.applied)}
|
||||
on:clear={() => (applied = 0)} />
|
||||
<hr />
|
||||
<div class="u-flex u-margin-block-start-16 u-main-end u-gap-8">
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
</div>
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)} {fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
<span class="inline-tag">
|
||||
{applied}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</slot>
|
||||
|
||||
<Modal
|
||||
title="Filters"
|
||||
description="Apply filter rules to refine the table view"
|
||||
bind:show={showFiltersMobile}
|
||||
size="big">
|
||||
<Content
|
||||
{columns}
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
on:apply={(e) => (applied = e.detail.applied)}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{#if displayQuickFilters}
|
||||
<slot name="quick" />
|
||||
{:else}
|
||||
<Content
|
||||
{columns}
|
||||
bind:columnId={selectedColumn}
|
||||
bind:operatorKey
|
||||
bind:value
|
||||
bind:arrayValues
|
||||
{singleCondition}
|
||||
on:apply={afterApply}
|
||||
on:clear={() => (applied = 0)} />
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
<div
|
||||
class="u-flex u-cross-center u-width-full-line"
|
||||
class:u-main-end={!quickFilters}
|
||||
class:u-main-space-between={quickFilters}>
|
||||
{#if quickFilters}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => (displayQuickFilters = !displayQuickFilters)}
|
||||
class="u-margin-block-end-auto">
|
||||
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="u-flex u-gap-8">
|
||||
{#if singleCondition}
|
||||
<Button text on:click={() => (showFiltersMobile = false)}>Cancel</Button>
|
||||
{:else}
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
{/if}
|
||||
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as Filters } from './filters.svelte';
|
||||
export { default as TagList } from './tagList.svelte';
|
||||
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { page } from '$app/stores';
|
||||
import deepEqual from 'deep-equal';
|
||||
import type { Column, ColumnType } from '$lib/helpers/types';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
|
||||
export type TagValue = {
|
||||
tag: string;
|
||||
@@ -12,23 +13,23 @@ export type TagValue = {
|
||||
};
|
||||
|
||||
export type Operator = {
|
||||
toTag: (attribute: string, input?: string | number | string[]) => string | TagValue;
|
||||
toTag: (attribute: string, input?: string | number | string[], type?: string) => TagValue;
|
||||
toQuery: (attribute: string, input?: string | number | string[]) => string;
|
||||
types: ColumnType[];
|
||||
hideInput?: boolean;
|
||||
};
|
||||
|
||||
export function mapToQueryParams(map: Map<string | TagValue, string>) {
|
||||
export function mapToQueryParams(map: Map<TagValue, string>) {
|
||||
return encodeURIComponent(JSON.stringify(Array.from(map.entries())));
|
||||
}
|
||||
|
||||
export function queryParamToMap(queryParam: string) {
|
||||
const decodedQueryParam = decodeURIComponent(queryParam);
|
||||
const queries = JSON.parse(decodedQueryParam) as [string, string][];
|
||||
const queries = JSON.parse(decodedQueryParam) as [TagValue, string][];
|
||||
return new Map(queries);
|
||||
}
|
||||
|
||||
function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
function initQueries(initialValue = new Map<TagValue, string>()) {
|
||||
const queries = writable(initialValue);
|
||||
|
||||
type AddFilterArgs = {
|
||||
@@ -39,12 +40,15 @@ function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
|
||||
function addFilter({ column, operator, value }: AddFilterArgs) {
|
||||
queries.update((map) => {
|
||||
map.set(operator.toTag(column.id, value), operator.toQuery(column.id, value));
|
||||
map.set(
|
||||
operator.toTag(column.title, value, column?.type),
|
||||
operator.toQuery(column.id, value)
|
||||
);
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
function removeFilter(tag: string | TagValue) {
|
||||
function removeFilter(tag: TagValue) {
|
||||
queries.update((map) => {
|
||||
map.delete(tag);
|
||||
return map;
|
||||
@@ -58,7 +62,7 @@ function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
function apply() {
|
||||
const queryParam = mapToQueryParams(get(queries));
|
||||
const currentLocation = window.location.pathname;
|
||||
goto(`${currentLocation}?query=${queryParam}`);
|
||||
goto(`${currentLocation}?query=${queryParam}`, { noScroll: true });
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -96,7 +100,6 @@ export function addFilter(
|
||||
) {
|
||||
const operator = operatorKey ? operators[operatorKey] : null;
|
||||
const column = columns.find((c) => c.id === columnId) as Column;
|
||||
|
||||
if (!column || !operator) return;
|
||||
if (column.array) {
|
||||
queries.addFilter({ column, operator, value: arrayValues });
|
||||
@@ -105,74 +108,133 @@ export function addFilter(
|
||||
}
|
||||
}
|
||||
|
||||
export const operators: Record<string, Operator> = {
|
||||
'starts with': {
|
||||
toQuery: Query.startsWith,
|
||||
toTag: (attribute, input) => `**${attribute}** starts with **${input}**`,
|
||||
types: ['string']
|
||||
},
|
||||
'ends with': {
|
||||
toQuery: Query.endsWith,
|
||||
toTag: (attribute, input) => `**${attribute}** ends with **${input}**`,
|
||||
types: ['string']
|
||||
},
|
||||
'greater than': {
|
||||
toQuery: (attr, input) => Query.greaterThan(attr, Number(input)),
|
||||
toTag: (attribute, input) => `**${attribute}** greater than **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'greater than or equal': {
|
||||
toQuery: (attr, input) => Query.greaterThanEqual(attr, Number(input)),
|
||||
toTag: (attribute, input) => `**${attribute}** greater than or equal to **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'less than': {
|
||||
toQuery: Query.lessThan,
|
||||
toTag: (attribute, input) => `**${attribute}** less than **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'less than or equal': {
|
||||
toQuery: Query.lessThanEqual,
|
||||
toTag: (attribute, input) => `**${attribute}** less than or equal to **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
equal: {
|
||||
toQuery: Query.equal,
|
||||
toTag: (attribute, input) => `**${attribute}** equal to **${input}**`,
|
||||
types: ['string', 'integer', 'double', 'boolean']
|
||||
},
|
||||
'not equal': {
|
||||
toQuery: Query.notEqual,
|
||||
toTag: (attribute, input) => `**${attribute}** not equal to **${input}**`,
|
||||
types: ['string', 'integer', 'double', 'boolean']
|
||||
},
|
||||
'is not null': {
|
||||
toQuery: Query.isNotNull,
|
||||
toTag: (attribute) => `**${attribute}** is not null`,
|
||||
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'],
|
||||
hideInput: true
|
||||
},
|
||||
'is null': {
|
||||
toQuery: Query.isNull,
|
||||
toTag: (attribute) => `**${attribute}** is null`,
|
||||
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'],
|
||||
hideInput: true
|
||||
},
|
||||
contains: {
|
||||
toQuery: Query.contains,
|
||||
toTag: (attribute, input) => {
|
||||
if (Array.isArray(input) && input.length > 2) {
|
||||
return {
|
||||
value: input,
|
||||
tag: `**${attribute}** contains **${formatArray(input)}** `
|
||||
};
|
||||
} else {
|
||||
return `**${attribute}** contains **${input}**`;
|
||||
}
|
||||
},
|
||||
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'enum']
|
||||
export enum ValidOperators {
|
||||
StartsWith = 'starts with',
|
||||
EndsWith = 'ends with',
|
||||
GreaterThan = 'greater than',
|
||||
GreaterThanOrEqual = 'greater than or equal',
|
||||
LessThan = 'less than',
|
||||
LessThanOrEqual = 'less than or equal',
|
||||
Equal = 'equal',
|
||||
NotEqual = 'not equal',
|
||||
IsNotNull = 'is not null',
|
||||
IsNull = 'is null',
|
||||
Contains = 'contains'
|
||||
}
|
||||
|
||||
export enum ValidTypes {
|
||||
String = 'string',
|
||||
Integer = 'integer',
|
||||
Double = 'double',
|
||||
Boolean = 'boolean',
|
||||
Datetime = 'datetime',
|
||||
Relationship = 'relationship',
|
||||
Enum = 'enum'
|
||||
}
|
||||
|
||||
const operatorsDefault = new Map<
|
||||
ValidOperators,
|
||||
{
|
||||
query: (attr: string, input: string | number | string[]) => string;
|
||||
types: ColumnType[];
|
||||
hideInput?: boolean;
|
||||
}
|
||||
};
|
||||
>([
|
||||
[ValidOperators.StartsWith, { query: Query.startsWith, types: [ValidTypes.String] }],
|
||||
[ValidOperators.EndsWith, { query: Query.endsWith, types: [ValidTypes.String] }],
|
||||
[
|
||||
ValidOperators.GreaterThan,
|
||||
{
|
||||
query: Query.greaterThan,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.GreaterThanOrEqual,
|
||||
{
|
||||
query: Query.greaterThanEqual,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.LessThan,
|
||||
{
|
||||
query: Query.lessThan,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.LessThanOrEqual,
|
||||
{
|
||||
query: Query.lessThanEqual,
|
||||
types: [ValidTypes.Integer, ValidTypes.Double, ValidTypes.Datetime]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.Equal,
|
||||
{
|
||||
query: Query.equal,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Enum
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.NotEqual,
|
||||
{
|
||||
query: Query.notEqual,
|
||||
types: [ValidTypes.String, ValidTypes.Integer, ValidTypes.Double, ValidTypes.Boolean]
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.IsNotNull,
|
||||
{
|
||||
query: Query.isNotNull,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Datetime,
|
||||
ValidTypes.Relationship
|
||||
],
|
||||
hideInput: true
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.IsNull,
|
||||
{
|
||||
query: Query.isNull,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Datetime,
|
||||
ValidTypes.Relationship
|
||||
],
|
||||
hideInput: true
|
||||
}
|
||||
],
|
||||
[
|
||||
ValidOperators.Contains,
|
||||
{
|
||||
query: Query.contains,
|
||||
types: [
|
||||
ValidTypes.String,
|
||||
ValidTypes.Integer,
|
||||
ValidTypes.Double,
|
||||
ValidTypes.Boolean,
|
||||
ValidTypes.Datetime,
|
||||
ValidTypes.Enum
|
||||
]
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
function formatArray(array: string[]) {
|
||||
if (!array?.length) return;
|
||||
@@ -182,3 +244,41 @@ function formatArray(array: string[]) {
|
||||
return array.join(' or ');
|
||||
}
|
||||
}
|
||||
|
||||
function generateDefaultOperators() {
|
||||
const operators: Record<string, Operator> = {};
|
||||
operatorsDefault.forEach((operator, operatorName) => {
|
||||
operators[operatorName] = {
|
||||
toQuery: operator.query,
|
||||
toTag: (attribute, input = null, type = null) => {
|
||||
if (input === null) {
|
||||
return {
|
||||
value: '',
|
||||
tag: `**${attribute}** ${operatorName}`
|
||||
};
|
||||
} else if (Array.isArray(input) && input.length > 2) {
|
||||
return {
|
||||
value: input,
|
||||
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
|
||||
};
|
||||
} else if (type === ValidTypes.Datetime) {
|
||||
return {
|
||||
value: input,
|
||||
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
|
||||
};
|
||||
} else {
|
||||
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
|
||||
}
|
||||
},
|
||||
types: operator.types,
|
||||
hideInput: operator.hideInput
|
||||
};
|
||||
});
|
||||
return operators;
|
||||
}
|
||||
|
||||
export const operators = generateDefaultOperators();
|
||||
|
||||
export function tagFormat(node: HTMLElement) {
|
||||
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { queries, tagFormat, tags } from './store';
|
||||
</script>
|
||||
|
||||
{#each $tags as tag (tag)}
|
||||
<button
|
||||
use:tooltip={{
|
||||
content: tag?.value?.toString(),
|
||||
disabled: Array.isArray(tag.value) ? tag.value?.length < 3 : true
|
||||
}}
|
||||
type="button"
|
||||
class="tag"
|
||||
on:click|preventDefault={() => {
|
||||
queries.removeFilter(tag);
|
||||
queries.apply();
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag.tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{/each}
|
||||
@@ -5,6 +5,7 @@
|
||||
export let tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
export let size: Size;
|
||||
export let trimmed = true;
|
||||
export let trimmedSecondLine = false;
|
||||
export let id: string = null;
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
@@ -15,6 +16,7 @@
|
||||
this={tag}
|
||||
class={`heading-level-${size} u-min-width-0 ${classes}`}
|
||||
class:u-trim-1={trimmed}
|
||||
class:u-trim-2={trimmedSecondLine}
|
||||
{style}
|
||||
{id}>
|
||||
<slot />
|
||||
|
||||
@@ -56,7 +56,7 @@ export { default as PaginationWithLimit } from './paginationWithLimit.svelte';
|
||||
export { default as ClickableList } from './clickableList.svelte';
|
||||
export { default as ClickableListItem } from './clickableListItem.svelte';
|
||||
export { default as Id } from './id.svelte';
|
||||
export { default as ProgressBar } from './progressBar.svelte';
|
||||
export * from './progressbar';
|
||||
export { default as ProgressBarBig } from './progressBarBig.svelte';
|
||||
export { default as CreditCardInfo } from './creditCardInfo.svelte';
|
||||
export { default as CreditCardBrandImage } from './creditCardBrandImage.svelte';
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<InputSelect
|
||||
wrapperTag="div"
|
||||
id="rows"
|
||||
label="Rows per page"
|
||||
label={`${name} per page`}
|
||||
showLabel={false}
|
||||
{options}
|
||||
bind:value={limit}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let factors: Models.MfaFactors & { recoveryCode: boolean };
|
||||
/** If true, the form will be submitted automatically when the code is entered. */
|
||||
@@ -40,8 +41,16 @@
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
disabled = true;
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
disabled = false;
|
||||
try {
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Form } from '$lib/elements/forms';
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' | 'huge' = null;
|
||||
@@ -19,6 +20,10 @@
|
||||
|
||||
let alert: HTMLElement;
|
||||
|
||||
beforeNavigate(() => {
|
||||
show = false;
|
||||
});
|
||||
|
||||
$: $disableCommands(show);
|
||||
|
||||
$: if (error) {
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<script lang="ts">
|
||||
export let currentValue: string;
|
||||
export let currentUnit: string;
|
||||
export let maxValue: string;
|
||||
export let maxUnit: string;
|
||||
import { ProgressBar, type ProgressbarData } from '$lib/components/progressbar';
|
||||
|
||||
export let currentValue: string | undefined = undefined;
|
||||
export let currentUnit: string | undefined = undefined;
|
||||
export let maxValue: string | undefined = undefined;
|
||||
export let maxUnit: string | undefined = undefined;
|
||||
export let progressValue: number;
|
||||
export let progressMax: number;
|
||||
export let showBar = true;
|
||||
export let minimum = 0;
|
||||
export let maximum = 100;
|
||||
export let progressBarData: Array<ProgressbarData> = [];
|
||||
|
||||
$: progress = Math.round((progressValue / progressMax) * 100);
|
||||
</script>
|
||||
|
||||
<section class="progress-bar">
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{currentValue}</span>
|
||||
<span class="body-text-1 u-bold">{currentUnit}</span>
|
||||
{#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined && maxValue !== undefined && maxUnit !== undefined}
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p>
|
||||
<span class="heading-level-4">{currentValue}</span>
|
||||
<span class="body-text-1 u-bold">{currentUnit}</span>
|
||||
</p>
|
||||
<p class="heading-level-4">{progress}%</p>
|
||||
</div>
|
||||
|
||||
<p class="body-text-2">
|
||||
{maxValue}
|
||||
{maxUnit}
|
||||
</p>
|
||||
<p class="heading-level-4">{progress}%</p>
|
||||
</div>
|
||||
<p class="body-text-2">
|
||||
{maxValue}
|
||||
{maxUnit}
|
||||
</p>
|
||||
</div>
|
||||
{#if showBar}
|
||||
<div
|
||||
class="progress-bar-container u-margin-block-start-16"
|
||||
class:is-warning={progress >= 75 && progress < 100}
|
||||
class:is-danger={progress >= 100}
|
||||
style:--graph-size={Math.max(Math.min(progress, maximum), minimum) + '%'} />
|
||||
{/if}
|
||||
{#if showBar && progressBarData.length > 0}
|
||||
<ProgressBar maxSize={progressMax} data={progressBarData} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import type { ProgressbarData, ProgressbarProps } from '$lib/components';
|
||||
|
||||
type $$Props = ProgressbarProps;
|
||||
|
||||
/**
|
||||
* The max value of the progressbar
|
||||
*/
|
||||
export let maxSize: $$Props['maxSize'];
|
||||
|
||||
/**
|
||||
* The data for the progressbar
|
||||
*/
|
||||
export let data: $$Props['data'];
|
||||
|
||||
/**
|
||||
* The remaining value of the progressbar
|
||||
*/
|
||||
$: remainder = data.reduce((sum: number, item: ProgressbarData) => sum - item.size, maxSize);
|
||||
</script>
|
||||
|
||||
<section class="progressbar__container">
|
||||
{#each $$props.data as item}
|
||||
<div
|
||||
class="progressbar__content"
|
||||
style:background-color={item.color}
|
||||
style:width={`${(item.size / maxSize) * 100}%`}
|
||||
use:tooltip={{
|
||||
disabled: !item.tooltip,
|
||||
allowHTML: true,
|
||||
content: `<span class="u-bold">${item.tooltip.title}</span> ${item.tooltip.label}`
|
||||
}}>
|
||||
</div>
|
||||
{/each}
|
||||
{#if remainder > 0}
|
||||
<div class="progressbar__content" style:width={`${(remainder / maxSize) * 100}%`} />
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
:root {
|
||||
--progressbar-border-radius: 0.25rem;
|
||||
--progressbar-tooltip-label-color: #818186;
|
||||
--progressbar-tooltip-link-color: #6c6c71;
|
||||
}
|
||||
|
||||
:global(.theme-dark) {
|
||||
--progressbar-background-color: var(--neutral-800, #2d2d31);
|
||||
--progressbar-tooltip-background-color: var(--neutral-800, #2d2d31);
|
||||
--progressbar-tooltip-border-color: var(--neutral-80, #424248);
|
||||
}
|
||||
:global(.theme-light) {
|
||||
--progressbar-background-color: var(--neutral-40, #f4f4f7);
|
||||
--progressbar-tooltip-background-color: #ffffff;
|
||||
--progressbar-tooltip-border-color: #ededf0;
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
&__container {
|
||||
height: 0.5rem;
|
||||
background-color: var(--progressbar-background-color);
|
||||
border-radius: var(--progressbar-border-radius);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
height: 100%;
|
||||
min-width: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 2.5rem;
|
||||
margin-top: -1.25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: var(--progressbar-border-radius);
|
||||
border-bottom-left-radius: var(--progressbar-border-radius);
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: var(--progressbar-border-radius);
|
||||
border-bottom-right-radius: var(--progressbar-border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
export type ProgressbarData = {
|
||||
size: number;
|
||||
color: string;
|
||||
tooltip?: {
|
||||
title: string;
|
||||
label: string;
|
||||
// linkTitle?: string;
|
||||
// linkPath?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProgressbarProps = {
|
||||
maxSize: number;
|
||||
data: Array<ProgressbarData>;
|
||||
};
|
||||
|
||||
export { default as ProgressBar } from './ProgressBar.svelte';
|
||||
@@ -6,15 +6,15 @@
|
||||
export let center = false;
|
||||
</script>
|
||||
|
||||
<li class="secondary-tabs-item" class:u-stretch={stretch}>
|
||||
<li class="secondary-tabs-item" class:u-stretch={stretch} role="tab">
|
||||
{#if href}
|
||||
{#if disabled}
|
||||
<button
|
||||
disabled
|
||||
type="button"
|
||||
class="secondary-tabs-button"
|
||||
class:u-width-full-line={fullWidth}
|
||||
class:u-text-center={center}
|
||||
disabled
|
||||
type="button">
|
||||
class:u-text-center={center}>
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{:else}
|
||||
@@ -28,10 +28,10 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="secondary-tabs-button"
|
||||
class:u-width-full-line={fullWidth}
|
||||
class:u-text-center={center}
|
||||
type="button"
|
||||
{disabled}
|
||||
on:click>
|
||||
<span class="text"><slot /></span>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import SupportWizard from '../../routes/console/supportWizard.svelte';
|
||||
import { showSupportModal } from '../../routes/console/wizard/support/store';
|
||||
import SupportWizard from '$routes/(console)/supportWizard.svelte';
|
||||
import { showSupportModal } from '$routes/(console)/wizard/support/store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
@@ -88,6 +88,7 @@
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/appwrite/appwrite/issues"
|
||||
aria-label="Open issue on GitHub"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="button is-secondary u-padding-inline-12 u-stretch u-main-center u-gap-4 u-flex-basis-auto">
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<li class="tabs-item">
|
||||
<li class="tabs-item" role="tab">
|
||||
{#if href}
|
||||
<a
|
||||
class="tabs-button"
|
||||
@@ -87,8 +87,7 @@
|
||||
class:is-selected={selected}
|
||||
on:click={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
on:keydown={handleKeyDown}>
|
||||
<span class="text"><slot /></span>
|
||||
</a>
|
||||
{:else}
|
||||
@@ -99,8 +98,7 @@
|
||||
on:click|preventDefault
|
||||
on:click={handleClick}
|
||||
tabindex={selected ? 0 : -1}
|
||||
on:keydown={handleKeyDown}
|
||||
role="tab">
|
||||
on:keydown={handleKeyDown}>
|
||||
<span class="text"><slot /></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
{/if}
|
||||
<ul
|
||||
class="tabs-list scroll-shadow-horizontal"
|
||||
role="tablist"
|
||||
bind:this={tabsList}
|
||||
on:scroll={throttle(onScroll, 25)}>
|
||||
<slot />
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
{#if file.completed || file.progress === 100}
|
||||
<a
|
||||
class="upload-box-item"
|
||||
href={`${base}/console/project-${$page.params.project}/storage/bucket-${$page.params.bucket}/file-${file.$id}`}>
|
||||
href={`${base}/project-${$page.params.project}/storage/bucket-${$page.params.bucket}/file-${file.$id}`}>
|
||||
<div class="u-margin-inline-end-16">
|
||||
<Avatar
|
||||
size={32}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
export let allowNoColumns = false;
|
||||
export let showColsTextMobile = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let hideText = false;
|
||||
|
||||
let showSelectColumns = false;
|
||||
|
||||
@@ -84,7 +85,10 @@
|
||||
class="icon-view-boards u-opacity-50"
|
||||
aria-hidden="true"
|
||||
aria-label="columns" />
|
||||
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}">Columns</span>
|
||||
{#if !hideText}
|
||||
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}"
|
||||
>Columns</span>
|
||||
{/if}
|
||||
<span class="inline-tag">{selectedColumnsNumber}</span>
|
||||
</Button>
|
||||
<svelte:fragment slot="list">
|
||||
|
||||
+77
-38
@@ -65,200 +65,239 @@ export const scopes: {
|
||||
scope: string;
|
||||
description: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
}[] = [
|
||||
{
|
||||
scope: 'sessions.write',
|
||||
description: "Access to create, update and delete your project's sessions",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'users.read',
|
||||
description: "Access to read your project's users",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'users.write',
|
||||
description: "Access to create, update, and delete your project's users",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'teams.read',
|
||||
description: "Access to read your project's teams",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'teams.write',
|
||||
description: "Access to create, update, and delete your project's teams",
|
||||
category: 'Auth'
|
||||
category: 'Auth',
|
||||
icon: 'user-group'
|
||||
},
|
||||
{
|
||||
scope: 'databases.read',
|
||||
description: "Access to read your project's databases",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'databases.write',
|
||||
description: "Access to create, update, and delete your project's databases",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'collections.read',
|
||||
description: "Access to read your project's database collections",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'collections.write',
|
||||
description: "Access to create, update, and delete your project's database collections",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'attributes.read',
|
||||
description: "Access to read your project's database collection's attributes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'attributes.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's database collection's attributes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'indexes.read',
|
||||
description: "Access to read your project's database collection's indexes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'indexes.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's database collection's indexes",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'documents.read',
|
||||
description: "Access to read your project's database documents",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'documents.write',
|
||||
description: "Access to create, update, and delete your project's database documents",
|
||||
category: 'Database'
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'files.read',
|
||||
description: "Access to read your project's storage files and preview images",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'files.write',
|
||||
description: "Access to create, update, and delete your project's storage files",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'buckets.read',
|
||||
description: "Access to read your project's storage buckets",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'buckets.write',
|
||||
description: "Access to create, update, and delete your project's storage buckets",
|
||||
category: 'Storage'
|
||||
category: 'Storage',
|
||||
icon: 'folder'
|
||||
},
|
||||
{
|
||||
scope: 'functions.read',
|
||||
description: "Access to read your project's functions and code deployments",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'functions.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's functions and code deployments",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'execution.read',
|
||||
description: "Access to read your project's execution logs",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'execution.write',
|
||||
description: "Access to execute your project's functions",
|
||||
category: 'Functions'
|
||||
category: 'Functions',
|
||||
icon: 'lightning-bolt'
|
||||
},
|
||||
{
|
||||
scope: 'targets.read',
|
||||
description: "Access to read your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'targets.write',
|
||||
description: "Access to create, update, and delete your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'providers.read',
|
||||
description: "Access to read your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'providers.write',
|
||||
description: "Access to create, update, and delete your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'messages.read',
|
||||
description: "Access to read your project's messages",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'messages.write',
|
||||
description: "Access to create, update, and delete your project's messages",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'topics.read',
|
||||
description: "Access to read your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'topics.write',
|
||||
description: "Access to create, update, and delete your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.read',
|
||||
description: "Access to read your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
category: 'Messaging',
|
||||
icon: 'send'
|
||||
},
|
||||
{
|
||||
scope: 'locale.read',
|
||||
description: "Access to access your project's Locale service",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'avatars.read',
|
||||
description: "Access to access your project's Avatars service",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'health.read',
|
||||
description: "Access to read your project's health status",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'migrations.read',
|
||||
description: "Access to read your project's migration status",
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
},
|
||||
{
|
||||
scope: 'migrations.write',
|
||||
description: 'Access to create migrations',
|
||||
category: 'Other'
|
||||
category: 'Other',
|
||||
icon: 'globe'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class={resolvedClasses}
|
||||
style="pointer-events: {internalDisabled ? 'none' : 'auto'};"
|
||||
aria-label={ariaLabel}
|
||||
use:multiAction={actions}>
|
||||
<slot />
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let tag: FormItemTag = 'li';
|
||||
export let fullWidth = false;
|
||||
export let isMultiple = false;
|
||||
export let stackOnMobile = false;
|
||||
export let style: string = '';
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
@@ -16,6 +17,7 @@
|
||||
class="form-item {classes}"
|
||||
{style}
|
||||
class:is-multiple={isMultiple}
|
||||
class:u-width-full-line={fullWidth}>
|
||||
class:u-width-full-line={fullWidth}
|
||||
class:u-flex-vertical-mobile={stackOnMobile}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
@@ -18,7 +18,18 @@
|
||||
class:icon-exclamation={type === 'warning'}
|
||||
aria-hidden="true" />
|
||||
{/if}
|
||||
<span class="text">
|
||||
<span class="text u-line-height-1-5">
|
||||
<slot />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<style lang="scss">
|
||||
.icon-info,
|
||||
.icon-exclamation-circle,
|
||||
.icon-check-circle,
|
||||
.icon-exclamation {
|
||||
&::before {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ export { default as InputSelectSearch } from './inputSelectSearch.svelte';
|
||||
export { default as InputCheckbox } from './inputCheckbox.svelte';
|
||||
export { default as InputChoice } from './inputChoice.svelte';
|
||||
export { default as InputPhone } from './inputPhone.svelte';
|
||||
export { default as InputOTP } from './inputOTP.svelte';
|
||||
export { default as InputCron } from './inputCron.svelte';
|
||||
export { default as InputURL } from './inputURL.svelte';
|
||||
export { default as InputId } from './inputId.svelte';
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
{#if (label && showLabel) || tooltip}
|
||||
<div class="u-flex u-gap-4">
|
||||
{#if label}
|
||||
<h6 class:u-hide={!showLabel} class="choice-item-title">
|
||||
<span class:u-hide={!showLabel} class="choice-item-title">
|
||||
{label}
|
||||
</h6>
|
||||
</span>
|
||||
{/if}
|
||||
{#if tooltip}
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
|
||||
export let label: string;
|
||||
@@ -16,6 +16,8 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let fullWidth = false;
|
||||
export let isMultiple = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -53,9 +55,10 @@
|
||||
}
|
||||
|
||||
$: isNullable = nullable && !required;
|
||||
$: wrapper = isMultiple ? FormItemPart : FormItem;
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
@@ -93,4 +96,4 @@
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
</svelte:component>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
endSegment,
|
||||
trigger
|
||||
},
|
||||
states: { months, headingValue, daysOfWeek, segmentContents, value },
|
||||
states: { months, headingValue, weekdays, segmentContents, value },
|
||||
helpers: { isDateDisabled, isDateUnavailable }
|
||||
} = createDateRangePicker();
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<table use:melt={$grid}>
|
||||
<thead aria-hidden="true">
|
||||
<tr>
|
||||
{#each $daysOfWeek as day}
|
||||
{#each $weekdays as day}
|
||||
<th class="dt-cell">
|
||||
<span class="u-flex u-main-center u-cross-center">
|
||||
{day}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let step: number | 'any' = 0.001;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -70,7 +71,7 @@
|
||||
{readonly}
|
||||
{required}
|
||||
{value}
|
||||
step=".001"
|
||||
{step}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="datetime-local"
|
||||
class="input-text"
|
||||
|
||||
@@ -32,21 +32,32 @@
|
||||
}
|
||||
|
||||
function isFileExtensionAllowed(fileExtension: string) {
|
||||
if (allowedFileExtensions.length && !allowedFileExtensions.includes(fileExtension)) {
|
||||
if (allowedFileExtensions?.length && !allowedFileExtensions.includes(fileExtension)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isFileOverSize(file: File) {
|
||||
if (maxSize && file.size > maxSize) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function dropHandler(ev: DragEvent) {
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
hovering = false;
|
||||
if (!ev.dataTransfer.items) return;
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
const fileExtension = ev.dataTransfer.items[i].getAsFile().name.split('.')[1];
|
||||
const fileExtension = ev.dataTransfer.items[i].getAsFile().name.split('.').pop();
|
||||
if (!isFileExtensionAllowed(fileExtension)) {
|
||||
error = 'Invalid file extension';
|
||||
return;
|
||||
}
|
||||
if (isFileOverSize(ev.dataTransfer.items[i].getAsFile())) {
|
||||
error = 'File size exceeds the limit';
|
||||
return;
|
||||
}
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(ev.dataTransfer.items[i].getAsFile());
|
||||
@@ -86,12 +97,18 @@
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
return isFileExtensionAllowed(fileExtension);
|
||||
});
|
||||
const isOverSize = maxSize && Array.from(target.files).some((file) => isFileOverSize(file));
|
||||
|
||||
if (!isValidFiles) {
|
||||
error = 'Invalid file extension';
|
||||
target.value = '';
|
||||
return;
|
||||
}
|
||||
if (isOverSize) {
|
||||
error = 'File size exceeds the limit';
|
||||
target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles(target.files);
|
||||
};
|
||||
@@ -183,15 +200,16 @@
|
||||
{#if files?.length}
|
||||
<ul class="upload-file-box-list u-min-width-0">
|
||||
{#each fileArray as file}
|
||||
{@const fileName = file.name.split('.')}
|
||||
{@const fileName = file.name.split('.').slice(0, -1).join('.')}
|
||||
{@const extension = file.name.split('.').pop()}
|
||||
{@const fileSize = humanFileSize(file.size)}
|
||||
<li class="u-flex u-cross-center u-min-width-0">
|
||||
<span class="icon-document" aria-hidden="true" />
|
||||
<span class="upload-file-box-name u-trim u-min-width-0">
|
||||
<Trim>{fileName[0]}</Trim>
|
||||
<Trim>{fileName}</Trim>
|
||||
</span>
|
||||
<span class="upload-file-box-name u-min-width-0 u-flex-shrink-0">
|
||||
.{fileName[1]}
|
||||
.{extension}
|
||||
</span>
|
||||
<span
|
||||
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script lang="ts">
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let name: string = id;
|
||||
export let value = '';
|
||||
export let pattern: string = null;
|
||||
export let patternError: string = '';
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
export let hideRequired = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let maxlength: number = null;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let fullWidth = false;
|
||||
export let tooltip: string = null;
|
||||
export let isMultiple = false;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
|
||||
if (patternError && element.validity.patternMismatch) {
|
||||
error = patternError;
|
||||
return;
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
|
||||
type $$Events = {
|
||||
input: Event & { target: HTMLInputElement };
|
||||
};
|
||||
|
||||
$: wrapper = isMultiple ? FormItemPart : FormItem;
|
||||
</script>
|
||||
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
{#if label}
|
||||
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style:font-size="var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{required}
|
||||
{maxlength}
|
||||
{pattern}
|
||||
type="text"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
class="input-text"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
{#if $$slots.options}
|
||||
<div class="options-list">
|
||||
<slot name="options" />
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning" class="u-line-height-1">{error}</Helper>
|
||||
{/if}
|
||||
</svelte:component>
|
||||
@@ -13,12 +13,13 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let minlength: number = null;
|
||||
export let maxlength: number = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
|
||||
const pattern = String.raw`^\+[1-9]\d{1,14}$`;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -33,15 +34,21 @@
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
error = "Allowed characters: leading '+' and maximum of 15 digits";
|
||||
return;
|
||||
}
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.validity.patternMismatch) {
|
||||
error = `Allowed characters: leading '+' and maximum of ${maxlength - 1} digits`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.validity.tooShort) {
|
||||
error = `The value must contain leading ‘+’ and at least ${minlength - 1} digits `;
|
||||
return;
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
@@ -83,6 +90,7 @@
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
{minlength}
|
||||
{maxlength}
|
||||
{pattern}
|
||||
{readonly}
|
||||
@@ -90,7 +98,13 @@
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
bind:value
|
||||
bind:this={element}
|
||||
style:--amount-of-buttons={$$slots.options ? 1 : 0}
|
||||
on:invalid={handleInvalid} />
|
||||
{#if $$slots.options}
|
||||
<div class="options-list">
|
||||
<slot name="options" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export let id: string;
|
||||
export let label: string | undefined = undefined;
|
||||
export let ariaLabel = label;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let value: string | number | boolean | null;
|
||||
@@ -62,6 +63,7 @@
|
||||
{id}
|
||||
{required}
|
||||
{disabled}
|
||||
aria-label={ariaLabel}
|
||||
bind:this={element}
|
||||
bind:value
|
||||
on:invalid={handleInvalid}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
export let fullWidth = false;
|
||||
export let autofocus = false;
|
||||
export let interactiveOutput = false;
|
||||
export let interactiveEmpty = false;
|
||||
export let hideEmpty = false;
|
||||
// stretch is used inside of a flex container to give the element flex:1
|
||||
export let stretch = true;
|
||||
export let search = '';
|
||||
@@ -51,7 +53,8 @@
|
||||
});
|
||||
|
||||
function handleInput() {
|
||||
hasFocus = true;
|
||||
if (hideEmpty && !options?.length) hasFocus = false;
|
||||
else hasFocus = true;
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
@@ -202,7 +205,18 @@
|
||||
</li>
|
||||
{:else}
|
||||
<li class="drop-list-item">
|
||||
<span class="text">There are no {name} that match your search</span>
|
||||
<button
|
||||
class:drop-button={interactiveEmpty}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (interactiveOutput && interactiveEmpty) hasFocus = !hasFocus;
|
||||
}}>
|
||||
<slot name="empty">
|
||||
<span class="text">
|
||||
There are no {name} that match your search
|
||||
</span>
|
||||
</slot>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -14,6 +14,9 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let fullWidth = false;
|
||||
export let isMultiple = false;
|
||||
export let step: number | 'any' = 60;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -38,9 +41,10 @@
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
$: wrapper = isMultiple ? FormItemPart : FormItem;
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
@@ -53,7 +57,7 @@
|
||||
{required}
|
||||
{min}
|
||||
{max}
|
||||
step="60"
|
||||
{step}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="time"
|
||||
class="input-text"
|
||||
@@ -68,4 +72,4 @@
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
</svelte:component>
|
||||
|
||||
@@ -2,3 +2,4 @@ export { default as Pill } from './pill.svelte';
|
||||
export { default as SelectSearchItem } from './selectSearchItem.svelte';
|
||||
export { default as Flag } from './flag.svelte';
|
||||
export { default as SelectSearchCheckbox } from './selectSearchCheckbox.svelte';
|
||||
export { default as SelectSearchRadio } from './selectSearchRadio.svelte';
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
export let external = false;
|
||||
export let href: string = null;
|
||||
export let event: string = null;
|
||||
export let eventData: Record<string, unknown> = {};
|
||||
export let style = '';
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
|
||||
function track() {
|
||||
if (!event) {
|
||||
@@ -19,17 +23,19 @@
|
||||
}
|
||||
|
||||
trackEvent(`click_${event}`, {
|
||||
from: 'tag'
|
||||
from: 'tag',
|
||||
...eventData
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{style}
|
||||
{href}
|
||||
target={external ? '_blank' : '_self'}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class="tag"
|
||||
class="tag {classes}"
|
||||
class:is-disabled={disabled}
|
||||
class:is-selected={selected}
|
||||
class:is-success={success}
|
||||
@@ -42,9 +48,10 @@
|
||||
<button
|
||||
on:click
|
||||
on:click={track}
|
||||
{style}
|
||||
{disabled}
|
||||
type={submit ? 'submit' : 'button'}
|
||||
class="tag"
|
||||
class="tag {classes}"
|
||||
class:is-disabled={disabled}
|
||||
class:is-selected={selected}
|
||||
class:is-success={success}
|
||||
@@ -55,7 +62,8 @@
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class="tag"
|
||||
class="tag {classes}"
|
||||
{style}
|
||||
class:is-disabled={disabled}
|
||||
class:is-selected={selected}
|
||||
class:is-success={success}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
// export let data: string[] = [];
|
||||
export let value = false;
|
||||
export let padding: number | null = null;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button u-flex u-cross-center u-gap-12"
|
||||
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
|
||||
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
|
||||
on:click|preventDefault
|
||||
type="button">
|
||||
<input type="checkbox" class="is-small" bind:checked={value} />
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let value: string;
|
||||
export let group: string;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button u-flex u-cross-center u-gap-12"
|
||||
on:click|preventDefault
|
||||
type="button">
|
||||
<input type="radio" class="is-small" {value} bind:group />
|
||||
<slot />
|
||||
</button>
|
||||
</li>
|
||||
@@ -45,7 +45,11 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="table-with-scroll {classes}" class:u-margin-block-start-32={!noMargin} data-private>
|
||||
<div
|
||||
class="table-with-scroll {classes}"
|
||||
style:border-radius={noStyles ? '0' : ''}
|
||||
class:u-margin-block-start-16={!noMargin}
|
||||
data-private>
|
||||
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
|
||||
<table
|
||||
class="table"
|
||||
|
||||
@@ -21,7 +21,7 @@ export enum View {
|
||||
export function getView(url: URL, route: Page['route'], fallback: View): View {
|
||||
return (url.searchParams.get('view') ?? preferences.get(route).view) === View.Grid
|
||||
? View.Grid
|
||||
: View.Table ?? fallback;
|
||||
: (View.Table ?? fallback);
|
||||
}
|
||||
|
||||
export function getColumns(route: Page['route'], fallback: string[]): string[] {
|
||||
|
||||
@@ -38,7 +38,7 @@ export type Column = {
|
||||
filter?: boolean;
|
||||
array?: boolean;
|
||||
format?: string;
|
||||
elements?: string[];
|
||||
elements?: string[] | { value: string | number; label: string }[];
|
||||
};
|
||||
|
||||
export function isValueOfStringEnum<T extends Record<string, string>>(
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
|
||||
export let title: string;
|
||||
export let tooltipContent =
|
||||
$organization.billingPlan === BillingPlan.FREE
|
||||
$organization?.billingPlan === BillingPlan.FREE
|
||||
? `Upgrade to add more ${title.toLocaleLowerCase()}`
|
||||
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
|
||||
tierToPlan($organization.billingPlan).name
|
||||
tierToPlan($organization?.billingPlan)?.name
|
||||
} plan`;
|
||||
export let disabled: boolean;
|
||||
export let buttonText: string;
|
||||
export let buttonMethod: () => void | Promise<void>;
|
||||
export let buttonHref: string = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let icon = 'plus';
|
||||
export let showIcon = true;
|
||||
@@ -31,7 +32,8 @@
|
||||
secondary={buttonType === 'secondary'}
|
||||
on:click={buttonMethod}
|
||||
event={buttonEvent}
|
||||
{disabled}>
|
||||
{disabled}
|
||||
href={buttonHref}>
|
||||
{#if showIcon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
{/if}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
export let buttonText: string = null;
|
||||
export let buttonMethod: () => void = null;
|
||||
export let buttonHref: string = null;
|
||||
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
|
||||
export let buttonDisabled = false;
|
||||
|
||||
@@ -177,7 +178,8 @@
|
||||
disabled={isButtonDisabled}
|
||||
{buttonText}
|
||||
{buttonEvent}
|
||||
{buttonMethod} />
|
||||
{buttonMethod}
|
||||
{buttonHref} />
|
||||
{/if}
|
||||
</slot>
|
||||
</header>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script>
|
||||
import { settings } from '$lib/components/consent.svelte';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { version } from '$routes/console/store';
|
||||
import { version } from '$routes/(console)/store';
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
</script>
|
||||
@@ -19,6 +17,7 @@
|
||||
<a
|
||||
class="text"
|
||||
href="https://github.com/appwrite/appwrite/releases"
|
||||
aria-label="Appwrite releases on Github"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Version {$version}
|
||||
@@ -43,14 +42,9 @@
|
||||
</li>
|
||||
{#if isCloud}
|
||||
<li class="inline-links-item">
|
||||
<span
|
||||
style:cursor="pointer"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click={() => settings.set(true)}>
|
||||
<a href="https://appwrite.io/cookies" target="_blank" rel="noreferrer">
|
||||
<span class="text">Cookies</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
@@ -61,14 +55,19 @@
|
||||
<span class="text">ⓒ {currentYear} Appwrite. All rights reserved.</span>
|
||||
</li>
|
||||
<li class="inline-links-item u-flex u-gap-8">
|
||||
<a href="https://github.com/appwrite/appwrite" target="_blank" rel="noreferrer">
|
||||
<span class="icon-github" aria-hidden="true" aria-label="Appwrite on Github" />
|
||||
<a
|
||||
href="https://github.com/appwrite/appwrite"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Appwrite on Github">
|
||||
<span class="icon-github" aria-hidden="true" />
|
||||
</a>
|
||||
<a href="https://appwrite.io/discord" target="_blank" rel="noreferrer">
|
||||
<span
|
||||
class="icon-discord"
|
||||
aria-hidden="true"
|
||||
aria-label="Appwrite on Discord" />
|
||||
<a
|
||||
href="https://appwrite.io/discord"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Appwrite on Discord">
|
||||
<span class="icon-discord" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user