mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into org-deletion-new-flow
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,39 @@
|
||||
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;
|
||||
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.31.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.13",
|
||||
"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.47.2",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.5.28",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@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.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.44.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.79.3",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"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.7.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.7",
|
||||
"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: 'https://dlbilling.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
+7609
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();
|
||||
@@ -184,6 +184,7 @@ export enum Submit {
|
||||
ProjectUpdateSMTP = 'submit_project_update_smtp',
|
||||
MemberCreate = 'submit_member_create',
|
||||
MemberDelete = 'submit_member_delete',
|
||||
MembershipUpdate = 'submit_membership_update',
|
||||
MembershipUpdateStatus = 'submit_membership_update_status',
|
||||
ProviderUpdate = 'submit_provider_update',
|
||||
TeamCreate = 'submit_team_create',
|
||||
@@ -194,6 +195,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 +229,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 +320,8 @@ 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',
|
||||
RequestBAA = 'submit_request_baa',
|
||||
RequestSoc2 = 'submit_request_soc2'
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -18,7 +19,7 @@
|
||||
let budgetEnabled = false;
|
||||
|
||||
$: currentPlan = $plansInfo.get(billingPlan);
|
||||
$: extraSeatsCost = (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
|
||||
$: extraSeatsCost = 0; // 0 untile trial period later replace (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
|
||||
$: grossCost = currentPlan.price + extraSeatsCost;
|
||||
$: estimatedTotal =
|
||||
couponData?.status === 'active'
|
||||
@@ -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}
|
||||
@@ -0,0 +1,414 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms/index';
|
||||
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
|
||||
import { app } from '$lib/stores/app';
|
||||
import {
|
||||
type BottomModalAlertItem,
|
||||
bottomModalAlerts,
|
||||
dismissBottomModalAlert,
|
||||
hideAllModalAlerts
|
||||
} from '$lib/stores/bottom-alerts';
|
||||
import { onMount } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
|
||||
import { project } from '$routes/(console)/project-[project]/store';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let currentIndex = 0;
|
||||
let openModalOnMobile = false;
|
||||
|
||||
function getPageScope(pathname: string) {
|
||||
const isProjectPage = pathname.includes('project-[project]');
|
||||
const isOrganizationPage = pathname.includes('organization-[organization]');
|
||||
|
||||
return { isProjectPage, isOrganizationPage };
|
||||
}
|
||||
|
||||
function filterModalAlerts(alerts: BottomModalAlertItem[], pathname: string) {
|
||||
const { isProjectPage, isOrganizationPage } = getPageScope(pathname);
|
||||
|
||||
return alerts
|
||||
.sort((a, b) => b.importance - a.importance)
|
||||
.filter((alert) => {
|
||||
return (
|
||||
alert.show &&
|
||||
shouldShowNotification(alert.id) &&
|
||||
// if no scope > show in projects & org pages.
|
||||
((!alert.scope && (isProjectPage || isOrganizationPage)) ||
|
||||
// project scope, show only in project pages
|
||||
(isProjectPage && alert.scope === 'project') ||
|
||||
// organization scope, show only in organization pages
|
||||
(isOrganizationPage && alert.scope === 'organization'))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
$: filteredModalAlerts = filterModalAlerts($bottomModalAlerts, $page.route.id);
|
||||
|
||||
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
|
||||
|
||||
function handleClose() {
|
||||
const modalAlert = currentModalAlert;
|
||||
dismissBottomModalAlert(modalAlert.id);
|
||||
hideNotification(modalAlert.id, { coolOffPeriod: 24 * 365 });
|
||||
if (modalAlert.closed) modalAlert.closed();
|
||||
|
||||
if (currentIndex === filteredModalAlerts.length - 1 && filteredModalAlerts.length > 1) {
|
||||
currentIndex = currentIndex - 1;
|
||||
} else {
|
||||
currentIndex = currentIndex % filteredModalAlerts.length;
|
||||
}
|
||||
}
|
||||
|
||||
function showNext() {
|
||||
currentIndex = (currentIndex + 1) % filteredModalAlerts.length;
|
||||
}
|
||||
|
||||
function showPrevious() {
|
||||
currentIndex = (currentIndex - 1 + filteredModalAlerts.length) % filteredModalAlerts.length;
|
||||
}
|
||||
|
||||
function showUpgrade() {
|
||||
const plan = currentModalAlert.plan;
|
||||
const organizationPlan = $organization.billingPlan;
|
||||
switch (plan) {
|
||||
case 'free':
|
||||
return false;
|
||||
case 'pro':
|
||||
return organizationPlan === BillingPlan.FREE;
|
||||
case 'scale':
|
||||
return (
|
||||
organizationPlan === BillingPlan.FREE || organizationPlan === BillingPlan.PRO
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
addBottomModalAlerts();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert}
|
||||
{@const shouldShowUpgrade = showUpgrade()}
|
||||
<div class="main-alert-wrapper is-not-mobile">
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
|
||||
fill="#97979B" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="content-wrapper u-flex-vertical u-gap-16">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={currentModalAlert.src.dark}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
|
||||
{:else}
|
||||
<img
|
||||
src={currentModalAlert.src.light}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
|
||||
{/if}
|
||||
|
||||
{#if filteredModalAlerts.length > 1}
|
||||
<div class="u-flex u-main-space-between u-cross-baseline">
|
||||
<span class="inline-tag feature-count-tag">
|
||||
Feature {currentIndex + 1} of {filteredModalAlerts.length}
|
||||
</span>
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
|
||||
<button
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex === filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex-vertical u-gap-4 u-padding-inline-8">
|
||||
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
{#if currentModalAlert.isHtml}
|
||||
{@html currentModalAlert.message}
|
||||
{:else}
|
||||
{currentModalAlert.message}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
class="button"
|
||||
href={shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
: currentModalAlert.cta.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}
|
||||
external={!!currentModalAlert.cta.external}
|
||||
fullWidthMobile>
|
||||
{currentModalAlert.cta.text}
|
||||
</Button>
|
||||
|
||||
{#if currentModalAlert.learnMore}
|
||||
<Button
|
||||
text
|
||||
class="button"
|
||||
external
|
||||
fullWidthMobile
|
||||
href={currentModalAlert.learnMore.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}>
|
||||
{currentModalAlert.learnMore.text}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-alert-wrapper is-only-mobile" class:closed={!openModalOnMobile}>
|
||||
{#if openModalOnMobile}
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
|
||||
fill="#97979B" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="content-wrapper u-flex-vertical u-gap-16">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={currentModalAlert.src.dark}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
|
||||
{:else}
|
||||
<img
|
||||
src={currentModalAlert.src.light}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
|
||||
{/if}
|
||||
|
||||
{#if filteredModalAlerts.length > 1}
|
||||
<div class="u-flex u-main-space-between u-cross-baseline">
|
||||
<span class="inline-tag feature-count-tag">
|
||||
Feature {currentIndex + 1} of {filteredModalAlerts.length}
|
||||
</span>
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
|
||||
<button
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex ===
|
||||
filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex-vertical u-gap-8 u-padding-inline-8">
|
||||
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
{#if currentModalAlert.isHtml}
|
||||
{@html currentModalAlert.message}
|
||||
{:else}
|
||||
{currentModalAlert.message}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
class="button"
|
||||
href={shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
: currentModalAlert.cta.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}
|
||||
external={!!currentModalAlert.cta.external}
|
||||
fullWidthMobile
|
||||
on:click={() => (openModalOnMobile = false)}>
|
||||
{shouldShowUpgrade
|
||||
? 'Upgrade plan'
|
||||
: currentModalAlert.cta.text}
|
||||
</Button>
|
||||
|
||||
{#if currentModalAlert.learnMore}
|
||||
<Button
|
||||
text
|
||||
class="button"
|
||||
external
|
||||
fullWidthMobile
|
||||
on:click={() => (openModalOnMobile = false)}
|
||||
href={currentModalAlert.learnMore.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
})}>
|
||||
{currentModalAlert.learnMore.text}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</article>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class:showing={!openModalOnMobile}
|
||||
class="card notification-card u-width-full-line"
|
||||
on:click={() => (openModalOnMobile = true)}>
|
||||
<div class="u-flex-vertical u-gap-4">
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<h3 class="body-text-2 u-bold">New features available</h3>
|
||||
<button on:click={hideAllModalAlerts}>
|
||||
<span class="icon-x" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
Explore new features to enhance your projects and improve security.
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.card {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.main-alert-wrapper {
|
||||
left: 1rem;
|
||||
z-index: 25;
|
||||
bottom: 1rem;
|
||||
position: fixed;
|
||||
max-width: 289px;
|
||||
}
|
||||
|
||||
.feature-count-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
width: fit-content;
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.icon-inline-tag {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
padding: var(--space-2, 4px);
|
||||
border-radius: var(--border-radius-S, 8px);
|
||||
border: hsl(var(--color-neutral-10)) solid 1px;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .icon-inline-tag {
|
||||
background: #1d1d21;
|
||||
border: hsl(var(--color-neutral-80)) solid 1px;
|
||||
}
|
||||
|
||||
.u-gap-10 {
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.icon-cheveron-left,
|
||||
.icon-cheveron-right {
|
||||
opacity: 0.5;
|
||||
color: #97979b;
|
||||
width: var(--icon-size-M, 20px);
|
||||
height: var(--icon-size-M, 20px);
|
||||
}
|
||||
|
||||
.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-alert-wrapper {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
max-width: 100vw;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(6px);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.main-alert-wrapper.closed {
|
||||
backdrop-filter: unset;
|
||||
}
|
||||
|
||||
.notification-card {
|
||||
padding: var(--space-5, 10px) var(--space-6, 12px);
|
||||
}
|
||||
|
||||
.main-alert-wrapper:has(.card.notification-card) {
|
||||
bottom: 0;
|
||||
top: unset;
|
||||
min-height: auto;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -53,7 +53,7 @@
|
||||
{style}
|
||||
on:click
|
||||
on:keyup={clickOnEnter}
|
||||
role={href || isButton ? 'button' : 'generic'}
|
||||
role={href || isButton ? 'button' : 'presentation'}
|
||||
{href}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import CardPlanLimit from './cardPlanLimit.svelte';
|
||||
|
||||
export let showEmpty = true;
|
||||
export let offset = 0;
|
||||
export let total = 0;
|
||||
export let event: string = null;
|
||||
@@ -27,7 +28,7 @@
|
||||
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
|
||||
{#if isCloud && serviceId && total >= planLimit}
|
||||
<CardPlanLimit {service} />
|
||||
{:else}
|
||||
{:else if showEmpty}
|
||||
<Empty on:click target={event}>
|
||||
<slot name="empty" />
|
||||
</Empty>
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
<style lang="scss">
|
||||
// TODO: remove once pink is updated
|
||||
.collapsible-item {
|
||||
.collapsible-wrapper {
|
||||
padding-left: 0;
|
||||
}
|
||||
.collapsible-wrapper.is-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</script>
|
||||
|
||||
<div class:box={isBox}>
|
||||
<div class="u-flex u-main-space-between u-cross-start">
|
||||
<div class="u-flex u-main-space-between u-cross-start" style="padding-block: 0.5rem;">
|
||||
<div class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2">
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
<p class="text u-bold">
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -5,14 +5,28 @@
|
||||
import EmptyDark from '$lib/images/empty-dark.svg';
|
||||
import { Heading } from '.';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let single = false;
|
||||
export let noMedia = false;
|
||||
export let target: string = null;
|
||||
export let href: string = null;
|
||||
export let marginTop = false;
|
||||
export let allowCreate = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function onClick(event) {
|
||||
if (!allowCreate) {
|
||||
return;
|
||||
}
|
||||
dispatch('click', event);
|
||||
}
|
||||
|
||||
function track() {
|
||||
if (!allowCreate) {
|
||||
return;
|
||||
}
|
||||
if (target) {
|
||||
trackEvent(`click_create_${target}`, {
|
||||
from: 'empty'
|
||||
@@ -28,13 +42,13 @@
|
||||
{#if !noMedia}
|
||||
<button
|
||||
type="button"
|
||||
on:click|preventDefault
|
||||
on:click={track}
|
||||
on:click={onClick}
|
||||
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}
|
||||
@@ -54,9 +68,11 @@
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel="create {target}">Documentation</Button>
|
||||
<Button secondary on:click on:click={track}>
|
||||
Create {target}
|
||||
</Button>
|
||||
{#if allowCreate}
|
||||
<Button secondary on:click on:click={track}>
|
||||
Create {target}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
feedbackOptions,
|
||||
feedbackData
|
||||
} from '$lib/stores/feedback';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
$: $selectedFeedback = feedbackOptions.find((option) => option.type === $feedback.type);
|
||||
|
||||
@@ -15,8 +18,11 @@
|
||||
await feedback.submitFeedback(
|
||||
`feedback-${$feedback.type}`,
|
||||
$feedbackData.message,
|
||||
$feedbackData.name,
|
||||
$feedbackData.email
|
||||
$user.name,
|
||||
$user.email,
|
||||
$organization.billingPlan,
|
||||
$page.url.href,
|
||||
$feedbackData.value
|
||||
);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
|
||||
import { FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import { feedbackData } from '$lib/stores/feedback';
|
||||
</script>
|
||||
|
||||
<FormList>
|
||||
<InputText label="Name" id="name" bind:value={$feedbackData.name} placeholder="Enter name" />
|
||||
<InputEmail
|
||||
label="Email"
|
||||
id="email"
|
||||
bind:value={$feedbackData.email}
|
||||
placeholder="Enter email" />
|
||||
<InputTextarea
|
||||
id="feedback"
|
||||
placeholder="Your message here"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
|
||||
import { FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import { feedbackData } from '$lib/stores/feedback';
|
||||
import Evaluation from './evaluation.svelte';
|
||||
</script>
|
||||
@@ -16,15 +16,5 @@
|
||||
required
|
||||
bind:value={$feedbackData.message}
|
||||
showLabel={false} />
|
||||
<InputText
|
||||
label="Name"
|
||||
id="name"
|
||||
bind:value={$feedbackData.name}
|
||||
placeholder="Enter name" />
|
||||
<InputEmail
|
||||
label="Email"
|
||||
id="email"
|
||||
bind:value={$feedbackData.email}
|
||||
placeholder="Enter email" />
|
||||
{/if}
|
||||
</FormList>
|
||||
|
||||
@@ -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';
|
||||
@@ -75,3 +75,4 @@ export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as EmptyCardImageCloud } from './emptyCardImageCloud.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
export { default as BottomModalAlert } from './bottomModalAlert.svelte';
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<InputSelect
|
||||
wrapperTag="div"
|
||||
id="rows"
|
||||
label="Rows per page"
|
||||
label={`${name} per page`}
|
||||
showLabel={false}
|
||||
{options}
|
||||
bind:value={limit}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
let inputDigitFields: InputDigits;
|
||||
|
||||
export async function verify(challenge: Models.MfaChallenge, code: string) {
|
||||
try {
|
||||
if (challenge == null) {
|
||||
@@ -10,6 +12,7 @@
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
} catch (error) {
|
||||
inputDigitFields?.clearInputsAndRefocus();
|
||||
trackError(error, Submit.AccountCreate);
|
||||
throw error;
|
||||
}
|
||||
@@ -24,6 +27,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 +44,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 () => {
|
||||
@@ -72,7 +84,12 @@
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p>A 6-digit verification code was sent to your phone, enter it below.</p>
|
||||
{/if}
|
||||
<InputDigits bind:value={code} required autofocus {autoSubmit} />
|
||||
<InputDigits
|
||||
bind:value={code}
|
||||
required
|
||||
autofocus
|
||||
{autoSubmit}
|
||||
bind:this={inputDigitFields} />
|
||||
{/if}
|
||||
{#if showVerifyButton}
|
||||
<FormItem>
|
||||
|
||||
@@ -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}
|
||||
<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 ? 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,93 @@
|
||||
<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;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&__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';
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
color: hsl(var(--color-neutral-50));
|
||||
line-height: 1.25rem;
|
||||
&:first-child {
|
||||
color: hsl(var(--color-neutral-70));
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) div {
|
||||
color: hsl(var(--color-neutral-20));
|
||||
|
||||
&:first-child {
|
||||
color: hsl(var(--color-neutral-10));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import Base from './base.svelte';
|
||||
</script>
|
||||
|
||||
<Base>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p>
|
||||
By default, all members are assigned a <span class="u-bold">Developer</span> role. You can
|
||||
change this at any time from your organization settings.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
class="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</a> about roles.
|
||||
</p>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import Base from './base.svelte';
|
||||
</script>
|
||||
|
||||
<Base>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p class="u-bold">Roles</p>
|
||||
<p>Owner, Developer, Editor, Analyst and Billing.</p>
|
||||
<p>
|
||||
<a
|
||||
class="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</a> about roles.
|
||||
</p>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import Base from './base.svelte';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
</script>
|
||||
|
||||
<Base>
|
||||
{#if isCloud}
|
||||
{#if $organization?.billingPlan !== BillingPlan.FREE}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p>
|
||||
<span class="u-bold">Roles</span>
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
<span class="inline-tag u-normal u-x-small">Pro plan</span>
|
||||
{/if}
|
||||
</p>
|
||||
<p>Owner, Developer, Editor, Analyst and Billing.</p>
|
||||
</div>
|
||||
<p>
|
||||
<Button link external href="https://appwrite.io/docs/advanced/platform/roles"
|
||||
>Learn more</Button> about roles.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p>
|
||||
<span class="u-bold">Roles</span>
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
<span class="inline-tag u-normal u-x-small">Pro plan</span>
|
||||
{/if}
|
||||
</p>
|
||||
<p>
|
||||
Upgrade to Pro to assign new roles to members such as Owner, Developer, Editor
|
||||
or Analyst.
|
||||
</p>
|
||||
</div>
|
||||
<p class="u-flex u-main-end u-cross-center u-gap-4">
|
||||
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
|
||||
>Learn more</Button>
|
||||
<Button secondary external href={$upgradeURL}>Upgrade plan</Button>
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<p>
|
||||
<span class="u-bold">Roles</span>
|
||||
<span class="inline-tag u-normal u-x-small">Appwrite Cloud</span>
|
||||
</p>
|
||||
<p>
|
||||
Upgrade to Cloud to assign new roles to members or ask us about our entriprise self
|
||||
hosted offering.
|
||||
</p>
|
||||
</div>
|
||||
<p class="u-flex u-main-end u-cross-center u-gap-4">
|
||||
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
|
||||
>Learn more</Button>
|
||||
<Button secondary external href={$upgradeURL}>Upgrade to Cloud</Button>
|
||||
</p>
|
||||
{/if}
|
||||
</Base>
|
||||
@@ -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">
|
||||
|
||||
+136
-38
@@ -61,204 +61,302 @@ export enum Dependencies {
|
||||
MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
|
||||
}
|
||||
|
||||
export const defaultScopes: string[] = [
|
||||
'global',
|
||||
'public',
|
||||
'home',
|
||||
'console',
|
||||
'graphql',
|
||||
'sessions.write',
|
||||
'account',
|
||||
'teams.read',
|
||||
'teams.write',
|
||||
'documents.read',
|
||||
'documents.write',
|
||||
'files.read',
|
||||
'files.write',
|
||||
'projects.read',
|
||||
'projects.write',
|
||||
'locale.read',
|
||||
'avatars.read',
|
||||
'execution.read',
|
||||
'execution.write',
|
||||
'targets.read',
|
||||
'targets.write',
|
||||
'subscribers.write',
|
||||
'subscribers.read',
|
||||
'assistant.read',
|
||||
'users.read',
|
||||
'users.write',
|
||||
'databases.read',
|
||||
'databases.write',
|
||||
'collections.read',
|
||||
'collections.write',
|
||||
'buckets.read',
|
||||
'buckets.write',
|
||||
'functions.read',
|
||||
'functions.write',
|
||||
'platforms.read',
|
||||
'platforms.write',
|
||||
'keys.read',
|
||||
'keys.write',
|
||||
'webhooks.read',
|
||||
'webhooks.write',
|
||||
'rules.read',
|
||||
'rules.write',
|
||||
'migrations.read',
|
||||
'migrations.write',
|
||||
'vcs.read',
|
||||
'vcs.write',
|
||||
'providers.read',
|
||||
'providers.write',
|
||||
'messages.read',
|
||||
'messages.write',
|
||||
'topics.read',
|
||||
'topics.write',
|
||||
'billing.read',
|
||||
'billing.write'
|
||||
];
|
||||
|
||||
export const defaultRoles: string[] = ['owner'];
|
||||
|
||||
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"
|
||||
|
||||
@@ -38,6 +38,21 @@
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clears the input fields and moves the focus to the first input.
|
||||
* Usually used when resetting fields on auth fails, etc.
|
||||
*/
|
||||
export function clearInputsAndRefocus() {
|
||||
value = '';
|
||||
autoSubmitted = false;
|
||||
|
||||
if (element) {
|
||||
const inputs = element.querySelectorAll('input');
|
||||
inputs.forEach((input) => (input.value = ''));
|
||||
if (autofocus) inputs[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
const input = element.querySelector('input');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
import type { FormItemTag } from './formItem.svelte';
|
||||
import { Drop } from '$lib/components';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
|
||||
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;
|
||||
@@ -18,10 +21,14 @@
|
||||
}[];
|
||||
export let isMultiple = false;
|
||||
export let fullWidth = false;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
|
||||
let element: HTMLSelectElement;
|
||||
let error: string;
|
||||
|
||||
let show: boolean = false;
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -53,7 +60,29 @@
|
||||
<svelte:component this={wrapper} {fullWidth} tag={wrapperTag}>
|
||||
{#if label}
|
||||
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<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:--card-border-radius="var(--border-radius-small)"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
@@ -62,6 +91,7 @@
|
||||
{id}
|
||||
{required}
|
||||
{disabled}
|
||||
aria-label={ariaLabel}
|
||||
bind:this={element}
|
||||
bind:value
|
||||
on:invalid={handleInvalid}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user