Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref

This commit is contained in:
Damodar Lohani
2024-09-19 13:46:38 +05:45
903 changed files with 16632 additions and 7036 deletions
+4 -5
View File
@@ -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=
+1
View File
@@ -7,3 +7,4 @@ node_modules
.env
.env.*
!.env.example
/playwright-report
+5 -3
View File
@@ -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:
+115
View File
@@ -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 }}"
+9 -7
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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 -4
View File
@@ -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
View File
@@ -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'
+40
View File
@@ -0,0 +1,40 @@
map $sent_http_content_type $expires {
# cache everything for 1 year
default 1y;
# html files shouldn't be cached for single-page applications
text/html off;
}
server {
listen 80;
listen [::]:80;
server_name localhost;
# serve compressed file if filename.gz exists
gzip_static on;
location /console {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /console/index.html;
# Add cache headers
expires $expires;
add_header Pragma public;
add_header Cache-Control "public";
# Deny IE browsers from going into quirks mode
add_header X-UA-Compatible "IE=Edge";
# X-Frame-Options is to prevent from clickJacking attack
add_header X-Frame-Options SAMEORIGIN;
# This header enables the Cross-site scripting (XSS) filter
add_header X-XSS-Protection "1; mode=block;";
# disable content-type sniffing on some browsers.
add_header X-Content-Type-Options nosniff;
}
location / {
absolute_redirect off;
return 301 /console;
}
}
+1937 -1351
View File
File diff suppressed because it is too large Load Diff
+43 -43
View File
@@ -1,7 +1,7 @@
{
"name": "@appwrite/console",
"engines": {
"node": ">=16"
"node": ">=20"
},
"scripts": {
"dev": "vite dev",
@@ -19,60 +19,60 @@
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "^0.6.2",
"@appwrite.io/console": "^1.2.0",
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@popperjs/core": "^2.11.8",
"@sentry/svelte": "^7.66.0",
"@sentry/tracing": "^7.66.0",
"@stripe/stripe-js": "^3.4.0",
"ai": "^2.2.11",
"analytics": "^0.8.9",
"@sentry/sveltekit": "^8.26.0",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.14",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.9",
"deep-equal": "^2.2.2",
"dotenv": "^16.3.1",
"echarts": "^5.4.3",
"nanoid": "^4.0.2",
"plausible-tracker": "^0.3.8",
"dayjs": "^1.11.12",
"deep-equal": "^2.2.3",
"echarts": "^5.5.1",
"envfile": "^7.1.0",
"nanoid": "^5.0.7",
"plausible-tracker": "^0.3.9",
"pretty-bytes": "^6.1.1",
"prismjs": "^1.29.0",
"svelte-confetti": "^1.3.0",
"svelte-confetti": "^1.4.0",
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.61.2",
"@playwright/test": "^1.44.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.3.4",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@testing-library/dom": "^9.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^4.0.3",
"@testing-library/user-event": "^14.4.3",
"@types/deep-equal": "^1.0.1",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"@vitest/ui": "^1.2.1",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.33.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.46.0",
"@sveltejs/adapter-static": "^3.0.4",
"@sveltejs/kit": "^2.5.22",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/svelte": "^5.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/deep-equal": "^1.0.4",
"@types/prismjs": "^1.26.4",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/ui": "^1.6.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.43.0",
"jsdom": "^22.1.0",
"kleur": "^4.1.5",
"prettier": "^3.2.2",
"prettier-plugin-svelte": "^3.1.2",
"sass": "^1.66.1",
"svelte": "^4.2.9",
"svelte-check": "^3.5.1",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"sass": "^1.77.8",
"svelte": "^4.2.18",
"svelte-check": "^3.8.5",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^5.0.4",
"svelte-preprocess": "^6.0.2",
"svelte-sequential-preprocessor": "^2.0.1",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^5.0.11",
"vitest": "^1.2.1"
"tslib": "^2.6.3",
"typescript": "^5.5.4",
"vite": "^5.4.0",
"vitest": "^1.6.0"
},
"type": "module"
"type": "module",
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
}
+6 -5
View File
@@ -4,19 +4,20 @@ const config: PlaywrightTestConfig = {
timeout: 120000,
reportSlowTests: null,
reporter: [['html', { open: 'never' }]],
retries: 3,
retries: 1,
use: {
baseURL: 'http://localhost:4173/console/',
trace: 'on-first-retry'
},
webServer: {
timeout: 120000,
env: {
VITE_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
VITE_CONSOLE_MODE: 'cloud',
VITE_STRIPE_PUBLIC_KEY:
PUBLIC_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
PUBLIC_CONSOLE_MODE: 'cloud',
PUBLIC_STRIPE_KEY:
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
},
command: 'npm run build && npm run preview',
command: 'pnpm run build && pnpm run preview',
port: 4173
}
};
+7575
View File
File diff suppressed because it is too large Load Diff
+35 -14
View File
@@ -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
View File
@@ -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
};
};
);
+14
View File
@@ -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();
+7 -1
View File
@@ -194,6 +194,8 @@ export enum Submit {
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
SessionsLengthUpdate = 'submit_sessions_length_update',
SessionsLimitUpdate = 'submit_sessions_limit_update',
SessionDelete = 'submit_session_delete',
@@ -226,13 +228,16 @@ export enum Submit {
FunctionUpdateLogging = 'submit_function_update_logging',
FunctionUpdateTimeout = 'submit_function_update_timeout',
FunctionUpdateEvents = 'submit_function_update_events',
FunctionUpdateScopes = 'submit_function_key_update_scopes',
FunctionConnectRepo = 'submit_function_connect_repo',
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
FunctionRedeploy = 'submit_function_redeploy',
DeploymentCreate = 'submit_deployment_create',
DeploymentDelete = 'submit_deployment_delete',
DeploymentUpdate = 'submit_deployment_update',
DeploymentCancel = 'submit_deployment_cancel',
ExecutionCreate = 'submit_execution_create',
ExecutionDelete = 'submit_execution_delete',
VariableCreate = 'submit_variable_create',
VariableDelete = 'submit_variable_delete',
VariableUpdate = 'submit_variable_update',
@@ -314,5 +319,6 @@ export enum Submit {
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete',
ApplyQuickFilter = 'submit_apply_quick_filter'
}
+2 -3
View File
@@ -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 = '';
+7 -6
View File
@@ -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
+3 -2
View File
@@ -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
+4 -3
View File
@@ -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'
+9 -8
View File
@@ -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'
}
+3 -2
View File
@@ -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;
+2 -1
View File
@@ -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;
+4 -3
View File
@@ -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)
+5 -4
View File
@@ -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
+3 -2
View File
@@ -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
+7 -6
View File
@@ -1,16 +1,17 @@
import { goto } from '$app/navigation';
import { sdk } from '$lib/stores/sdk';
import { project } from '$routes/console/project-[project]/store';
import { project } from '$routes/(console)/project-[project]/store';
import { get } from 'svelte/store';
import type { Command, Searcher } from '../commands';
import type { Models } from '@appwrite.io/console';
import { promptDeleteUser } from '$routes/console/project-[project]/auth/user-[user]/dangerZone.svelte';
import { promptDeleteUser } from '$routes/(console)/project-[project]/auth/user-[user]/dangerZone.svelte';
import { base } from '$app/paths';
const getUserCommand = (user: Models.User<Models.Preferences>, projectId: string) =>
({
label: user.name,
callback: () => {
goto(`/console/project-${projectId}/auth/user-${user.$id}`);
goto(`${base}/project-${projectId}/auth/user-${user.$id}`);
},
group: 'users',
icon: 'user-circle'
@@ -35,7 +36,7 @@ export const userSearcher = (async (query: string) => {
{
label: 'Go to activity',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/activity`);
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/activity`);
},
group: 'users',
nested: true
@@ -43,7 +44,7 @@ export const userSearcher = (async (query: string) => {
{
label: 'Go to sessions',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/sessions`);
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/sessions`);
},
group: 'users',
nested: true
@@ -51,7 +52,7 @@ export const userSearcher = (async (query: string) => {
{
label: 'Go to memberships',
callback: () => {
goto(`/console/project-${projectId}/auth/user-${users[0].$id}/memberships`);
goto(`${base}/project-${projectId}/auth/user-${users[0].$id}/memberships`);
},
group: 'users',
nested: true
@@ -20,10 +20,7 @@
plan. Consider upgrading to increase your resource usage.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
href={`${base}/console/organization-${$organization.$id}/usage`}
text
fullWidthMobile>
<Button href={`${base}/organization-${$organization.$id}/usage`} text fullWidthMobile>
<span class="text">View usage</span>
</Button>
<Button
@@ -1,10 +1,11 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { hideBillingHeaderRoutes } from '$lib/stores/billing';
import { orgMissingPaymentMethod } from '$routes/console/store';
import { orgMissingPaymentMethod } from '$routes/(console)/store';
</script>
{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes($page.url.pathname)}
@@ -16,9 +17,7 @@
your projects.
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
secondary
href={`/console/organization-${$orgMissingPaymentMethod.$id}/billing`}>
<Button secondary href={`${base}/organization-${$orgMissingPaymentMethod.$id}/billing`}>
Add payment method
</Button>
</svelte:fragment>
@@ -5,7 +5,7 @@
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { activeHeaderAlert } from '$routes/console/store';
import { activeHeaderAlert } from '$routes/(console)/store';
import GradientBanner from '../gradientBanner.svelte';
let show = true;
@@ -29,7 +29,7 @@
secondary
fullWidthMobile
class="u-line-height-1"
href={`${base}/console/apply-credit?code=appw50&org=${$organization.$id}`}
href={`${base}/apply-credit?code=appw50&org=${$organization.$id}`}
on:click={() => {
trackEvent('click_credits_redeem', {
from: 'button',
@@ -1,12 +1,12 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
import { actionRequiredInvoices, hideBillingHeaderRoutes } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { VARS } from '$lib/system';
const endpoint = VARS.APPWRITE_ENDPOINT ?? `${$page.url.origin}/v1`;
import { getApiEndpoint } from '$lib/stores/sdk';
const endpoint = getApiEndpoint();
</script>
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !hideBillingHeaderRoutes.includes($page.url.pathname)}
@@ -21,7 +21,7 @@
</Button>
<Button
secondary
href={`/console/organization-${$organization.$id}/billing?type=confirmation&invoice=${$actionRequiredInvoices.invoices[0].$id}`}>
href={`${base}/organization-${$organization.$id}/billing?type=confirmation&invoice=${$actionRequiredInvoices.invoices[0].$id}`}>
Authorize payment
</Button>
</svelte:fragment>
@@ -22,7 +22,7 @@
</svelte:fragment>
<svelte:fragment slot="buttons">
<Button
href={`${base}/console/organization-${$failedInvoice?.teamId}/billing#paymentMethods`}
href={`${base}/organization-${$failedInvoice?.teamId}/billing#paymentMethods`}
secondary
fullWidthMobile>
<span class="text">Update billing details</span>
@@ -11,6 +11,7 @@
export let couponData: Partial<Coupon>;
export let billingBudget: number;
export let fixedCoupon = false; // If true, the coupon cannot be removed
export let isDowngrade = false;
const today = new Date();
const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
@@ -41,8 +42,8 @@
<p class="text">{formatCurrency(currentPlan.price)}</p>
</span>
<span class="u-flex u-main-space-between">
<p class="text">Additional seats ({collaborators?.length})</p>
<p class="text">
<p class="text" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
<p class="text" class:u-bold={isDowngrade}>
{formatCurrency(extraSeatsCost)}
</p>
</span>
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@@ -7,10 +8,18 @@
<div class="top-banner">
<div class="top-banner-bg">
<div class="top-banner-bg-1">
<img src="/images/top-banner/bg-pink-desktop.svg" width="1283" height="1278" alt="" />
<img
src={`${base}/images/top-banner/bg-pink-desktop.svg`}
width="1283"
height="1278"
alt="" />
</div>
<div class="top-banner-bg-2">
<img src="/images/top-banner/bg-mint-desktop.svg" width="1051" height="1271" alt="" />
<img
src={`${base}/images/top-banner/bg-mint-desktop.svg`}
width="1051"
height="1271"
alt="" />
</div>
</div>
<div class="top-banner-content u-color-text-primary">
+1
View File
@@ -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>
+4 -2
View File
@@ -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}
+1 -1
View File
@@ -53,7 +53,7 @@
{style}
on:click
on:keyup={clickOnEnter}
role={href || isButton ? 'button' : 'generic'}
role={href || isButton ? 'button' : 'presentation'}
{href}>
<slot />
</svelte:element>
+1 -1
View File
@@ -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}
+7 -6
View File
@@ -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>
+3
View File
@@ -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}>
+2 -2
View File
@@ -32,9 +32,9 @@
on:click={track}
aria-label="create {target}">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" width="376" />
<img src={EmptyDark} alt="create" aria-hidden="true" height="242" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" width="376" />
<img src={EmptyLight} alt="create" aria-hidden="true" height="242" />
{/if}
</button>
{/if}
+3 -15
View File
@@ -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>
+1 -1
View File
@@ -3,7 +3,7 @@
import { EmptySearch } from '.';
import { queries } from './filters';
export let resource;
export let resource: string;
</script>
<EmptySearch hidePages>
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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>
+41 -58
View File
@@ -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">
+158 -47
View File
@@ -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
View File
@@ -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';
+175 -75
View File
@@ -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>');
}
+23
View File
@@ -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}
+2
View File
@@ -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 />
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -36,7 +36,7 @@
<InputSelect
wrapperTag="div"
id="rows"
label="Rows per page"
label={`${name} per page`}
showLabel={false}
{options}
bind:value={limit}
+11 -2
View File
@@ -24,6 +24,7 @@
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
import { addNotification } from '$lib/stores/notifications';
export let factors: Models.MfaFactors & { recoveryCode: boolean };
/** If true, the form will be submitted automatically when the code is entered. */
@@ -40,8 +41,16 @@
async function createChallenge(factor: AuthenticationFactor) {
disabled = true;
challengeType = factor;
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
disabled = false;
try {
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
} finally {
disabled = false;
}
}
onMount(async () => {
+5
View File
@@ -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) {
+23 -23
View File
@@ -1,36 +1,36 @@
<script lang="ts">
export let currentValue: string;
export let currentUnit: string;
export let maxValue: string;
export let maxUnit: string;
import { ProgressBar, type ProgressbarData } from '$lib/components/progressbar';
export let currentValue: string | undefined = undefined;
export let currentUnit: string | undefined = undefined;
export let maxValue: string | undefined = undefined;
export let maxUnit: string | undefined = undefined;
export let progressValue: number;
export let progressMax: number;
export let showBar = true;
export let minimum = 0;
export let maximum = 100;
export let progressBarData: Array<ProgressbarData> = [];
$: progress = Math.round((progressValue / progressMax) * 100);
</script>
<section class="progress-bar">
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<p>
<span class="heading-level-4">{currentValue}</span>
<span class="body-text-1 u-bold">{currentUnit}</span>
{#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined && maxValue !== undefined && maxUnit !== undefined}
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<p>
<span class="heading-level-4">{currentValue}</span>
<span class="body-text-1 u-bold">{currentUnit}</span>
</p>
<p class="heading-level-4">{progress}%</p>
</div>
<p class="body-text-2">
{maxValue}
{maxUnit}
</p>
<p class="heading-level-4">{progress}%</p>
</div>
<p class="body-text-2">
{maxValue}
{maxUnit}
</p>
</div>
{#if showBar}
<div
class="progress-bar-container u-margin-block-start-16"
class:is-warning={progress >= 75 && progress < 100}
class:is-danger={progress >= 100}
style:--graph-size={Math.max(Math.min(progress, maximum), minimum) + '%'} />
{/if}
{#if showBar && progressBarData.length > 0}
<ProgressBar maxSize={progressMax} data={progressBarData} />
{/if}
</section>
@@ -0,0 +1,92 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import type { ProgressbarData, ProgressbarProps } from '$lib/components';
type $$Props = ProgressbarProps;
/**
* The max value of the progressbar
*/
export let maxSize: $$Props['maxSize'];
/**
* The data for the progressbar
*/
export let data: $$Props['data'];
/**
* The remaining value of the progressbar
*/
$: remainder = data.reduce((sum: number, item: ProgressbarData) => sum - item.size, maxSize);
</script>
<section class="progressbar__container">
{#each $$props.data as item}
<div
class="progressbar__content"
style:background-color={item.color}
style:width={`${(item.size / maxSize) * 100}%`}
use:tooltip={{
disabled: !item.tooltip,
allowHTML: true,
content: `<span class="u-bold">${item.tooltip.title}</span> ${item.tooltip.label}`
}}>
</div>
{/each}
{#if remainder > 0}
<div class="progressbar__content" style:width={`${(remainder / maxSize) * 100}%`} />
{/if}
</section>
<style lang="scss">
:root {
--progressbar-border-radius: 0.25rem;
--progressbar-tooltip-label-color: #818186;
--progressbar-tooltip-link-color: #6c6c71;
}
:global(.theme-dark) {
--progressbar-background-color: var(--neutral-800, #2d2d31);
--progressbar-tooltip-background-color: var(--neutral-800, #2d2d31);
--progressbar-tooltip-border-color: var(--neutral-80, #424248);
}
:global(.theme-light) {
--progressbar-background-color: var(--neutral-40, #f4f4f7);
--progressbar-tooltip-background-color: #ffffff;
--progressbar-tooltip-border-color: #ededf0;
}
.progressbar {
&__container {
height: 0.5rem;
background-color: var(--progressbar-background-color);
border-radius: var(--progressbar-border-radius);
display: flex;
flex-direction: row;
gap: 2px;
}
&__content {
height: 100%;
min-width: 4px;
display: flex;
justify-content: center;
&::before {
content: '';
height: 2.5rem;
margin-top: -1.25rem;
width: 100%;
}
&:first-child {
border-top-left-radius: var(--progressbar-border-radius);
border-bottom-left-radius: var(--progressbar-border-radius);
}
&:last-child {
border-top-right-radius: var(--progressbar-border-radius);
border-bottom-right-radius: var(--progressbar-border-radius);
}
}
}
</style>
+17
View File
@@ -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';
+5 -5
View File
@@ -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>
+3 -2
View File
@@ -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">
+3 -5
View File
@@ -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}
+1
View File
@@ -51,6 +51,7 @@
{/if}
<ul
class="tabs-list scroll-shadow-horizontal"
role="tablist"
bind:this={tabsList}
on:scroll={throttle(onScroll, 25)}>
<slot />
+1 -1
View File
@@ -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}
+5 -1
View File
@@ -17,6 +17,7 @@
export let allowNoColumns = false;
export let showColsTextMobile = false;
export let fullWidthMobile = false;
export let hideText = false;
let showSelectColumns = false;
@@ -84,7 +85,10 @@
class="icon-view-boards u-opacity-50"
aria-hidden="true"
aria-label="columns" />
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}">Columns</span>
{#if !hideText}
<span class="text {showColsTextMobile ? '' : 'is-only-desktop'}"
>Columns</span>
{/if}
<span class="inline-tag">{selectedColumnsNumber}</span>
</Button>
<svelte:fragment slot="list">
+77 -38
View File
@@ -65,200 +65,239 @@ export const scopes: {
scope: string;
description: string;
category: string;
icon: string;
}[] = [
{
scope: 'sessions.write',
description: "Access to create, update and delete your project's sessions",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'users.read',
description: "Access to read your project's users",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'users.write',
description: "Access to create, update, and delete your project's users",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'teams.read',
description: "Access to read your project's teams",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'teams.write',
description: "Access to create, update, and delete your project's teams",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'databases.read',
description: "Access to read your project's databases",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'databases.write',
description: "Access to create, update, and delete your project's databases",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'collections.read',
description: "Access to read your project's database collections",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'collections.write',
description: "Access to create, update, and delete your project's database collections",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.write',
description:
"Access to create, update, and delete your project's database collection's attributes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.read',
description: "Access to read your project's database collection's indexes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.write',
description:
"Access to create, update, and delete your project's database collection's indexes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'documents.read',
description: "Access to read your project's database documents",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'documents.write',
description: "Access to create, update, and delete your project's database documents",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'files.read',
description: "Access to read your project's storage files and preview images",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'files.write',
description: "Access to create, update, and delete your project's storage files",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'buckets.read',
description: "Access to read your project's storage buckets",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'buckets.write',
description: "Access to create, update, and delete your project's storage buckets",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'functions.read',
description: "Access to read your project's functions and code deployments",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'functions.write',
description:
"Access to create, update, and delete your project's functions and code deployments",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'execution.read',
description: "Access to read your project's execution logs",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'execution.write',
description: "Access to execute your project's functions",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'targets.read',
description: "Access to read your project's messaging targets",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'targets.write',
description: "Access to create, update, and delete your project's messaging targets",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'providers.read',
description: "Access to read your project's messaging providers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'providers.write',
description: "Access to create, update, and delete your project's messaging providers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'messages.read',
description: "Access to read your project's messages",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'messages.write',
description: "Access to create, update, and delete your project's messages",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'topics.read',
description: "Access to read your project's messaging topics",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'topics.write',
description: "Access to create, update, and delete your project's messaging topics",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'subscribers.read',
description: "Access to read your project's messaging topic subscribers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'subscribers.write',
description:
"Access to create, update, and delete your project's messaging topic subscribers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'locale.read',
description: "Access to access your project's Locale service",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'avatars.read',
description: "Access to access your project's Avatars service",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'health.read',
description: "Access to read your project's health status",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'migrations.read',
description: "Access to read your project's migration status",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'migrations.write',
description: 'Access to create migrations',
category: 'Other'
category: 'Other',
icon: 'globe'
}
];
+1
View File
@@ -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 />
+3 -1
View File
@@ -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>
+12 -1
View File
@@ -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>
+1
View File
@@ -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';
+2 -2
View File
@@ -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
+6 -3
View File
@@ -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>
+2 -2
View File
@@ -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}
+2 -1
View File
@@ -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"
+23 -5
View File
@@ -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">
+121
View File
@@ -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 -->
&nbsp;<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>
+19 -5
View File
@@ -13,12 +13,13 @@
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
export let minlength: number = null;
export let maxlength: number = null;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
const pattern = String.raw`^\+[1-9]\d{1,14}$`;
let element: HTMLInputElement;
let error: string;
@@ -33,15 +34,21 @@
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.patternMismatch) {
error = "Allowed characters: leading '+' and maximum of 15 digits";
return;
}
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
if (element.validity.patternMismatch) {
error = `Allowed characters: leading '+' and maximum of ${maxlength - 1} digits`;
return;
}
if (element.validity.tooShort) {
error = `The value must contain leading + and at least ${minlength - 1} digits `;
return;
}
error = element.validationMessage;
};
@@ -83,6 +90,7 @@
{placeholder}
{disabled}
{required}
{minlength}
{maxlength}
{pattern}
{readonly}
@@ -90,7 +98,13 @@
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
bind:this={element}
style:--amount-of-buttons={$$slots.options ? 1 : 0}
on:invalid={handleInvalid} />
{#if $$slots.options}
<div class="options-list">
<slot name="options" />
</div>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
@@ -4,6 +4,7 @@
export let id: string;
export let label: string | undefined = undefined;
export let ariaLabel = label;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let value: string | number | boolean | null;
@@ -62,6 +63,7 @@
{id}
{required}
{disabled}
aria-label={ariaLabel}
bind:this={element}
bind:value
on:invalid={handleInvalid}
@@ -26,6 +26,8 @@
export let fullWidth = false;
export let autofocus = false;
export let interactiveOutput = false;
export let interactiveEmpty = false;
export let hideEmpty = false;
// stretch is used inside of a flex container to give the element flex:1
export let stretch = true;
export let search = '';
@@ -51,7 +53,8 @@
});
function handleInput() {
hasFocus = true;
if (hideEmpty && !options?.length) hasFocus = false;
else hasFocus = true;
}
function handleKeydown(event: KeyboardEvent) {
@@ -202,7 +205,18 @@
</li>
{:else}
<li class="drop-list-item">
<span class="text">There are no {name} that match your search</span>
<button
class:drop-button={interactiveEmpty}
type="button"
on:click={() => {
if (interactiveOutput && interactiveEmpty) hasFocus = !hasFocus;
}}>
<slot name="empty">
<span class="text">
There are no {name} that match your search
</span>
</slot>
</button>
</li>
{/each}
</svelte:fragment>
+8 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { FormItem, Helper, Label } from '.';
import { FormItem, FormItemPart, Helper, Label } from '.';
export let label: string;
export let showLabel = true;
@@ -14,6 +14,9 @@
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
export let fullWidth = false;
export let isMultiple = false;
export let step: number | 'any' = 60;
let element: HTMLInputElement;
let error: string;
@@ -38,9 +41,10 @@
$: if (value) {
error = null;
}
$: wrapper = isMultiple ? FormItemPart : FormItem;
</script>
<FormItem>
<svelte:component this={wrapper} {fullWidth}>
<Label {required} {optionalText} hide={!showLabel} for={id}>
{label}
</Label>
@@ -53,7 +57,7 @@
{required}
{min}
{max}
step="60"
{step}
autocomplete={autocomplete ? 'on' : 'off'}
type="time"
class="input-text"
@@ -68,4 +72,4 @@
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
</FormItem>
</svelte:component>
+1
View File
@@ -2,3 +2,4 @@ export { default as Pill } from './pill.svelte';
export { default as SelectSearchItem } from './selectSearchItem.svelte';
export { default as Flag } from './flag.svelte';
export { default as SelectSearchCheckbox } from './selectSearchCheckbox.svelte';
export { default as SelectSearchRadio } from './selectSearchRadio.svelte';
+12 -4
View File
@@ -12,6 +12,10 @@
export let external = false;
export let href: string = null;
export let event: string = null;
export let eventData: Record<string, unknown> = {};
export let style = '';
let classes = '';
export { classes as class };
function track() {
if (!event) {
@@ -19,17 +23,19 @@
}
trackEvent(`click_${event}`, {
from: 'tag'
from: 'tag',
...eventData
});
}
</script>
{#if href}
<a
{style}
{href}
target={external ? '_blank' : '_self'}
rel={external ? 'noopener noreferrer' : ''}
class="tag"
class="tag {classes}"
class:is-disabled={disabled}
class:is-selected={selected}
class:is-success={success}
@@ -42,9 +48,10 @@
<button
on:click
on:click={track}
{style}
{disabled}
type={submit ? 'submit' : 'button'}
class="tag"
class="tag {classes}"
class:is-disabled={disabled}
class:is-selected={selected}
class:is-success={success}
@@ -55,7 +62,8 @@
</button>
{:else}
<div
class="tag"
class="tag {classes}"
{style}
class:is-disabled={disabled}
class:is-selected={selected}
class:is-success={success}
+3 -1
View File
@@ -1,11 +1,13 @@
<script lang="ts">
// export let data: string[] = [];
export let value = false;
export let padding: number | null = null;
</script>
<li class="drop-list-item">
<button
class="drop-button u-flex u-cross-center u-gap-12"
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
on:click|preventDefault
type="button">
<input type="checkbox" class="is-small" bind:checked={value} />
+14
View File
@@ -0,0 +1,14 @@
<script lang="ts">
export let value: string;
export let group: string;
</script>
<li class="drop-list-item">
<button
class="drop-button u-flex u-cross-center u-gap-12"
on:click|preventDefault
type="button">
<input type="radio" class="is-small" {value} bind:group />
<slot />
</button>
</li>
+5 -1
View File
@@ -45,7 +45,11 @@
};
</script>
<div class="table-with-scroll {classes}" class:u-margin-block-start-32={!noMargin} data-private>
<div
class="table-with-scroll {classes}"
style:border-radius={noStyles ? '0' : ''}
class:u-margin-block-start-16={!noMargin}
data-private>
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
<table
class="table"
+1 -1
View File
@@ -21,7 +21,7 @@ export enum View {
export function getView(url: URL, route: Page['route'], fallback: View): View {
return (url.searchParams.get('view') ?? preferences.get(route).view) === View.Grid
? View.Grid
: View.Table ?? fallback;
: (View.Table ?? fallback);
}
export function getColumns(route: Page['route'], fallback: string[]): string[] {
+1 -1
View File
@@ -38,7 +38,7 @@ export type Column = {
filter?: boolean;
array?: boolean;
format?: string;
elements?: string[];
elements?: string[] | { value: string | number; label: string }[];
};
export function isValueOfStringEnum<T extends Record<string, string>>(
+5 -3
View File
@@ -7,14 +7,15 @@
export let title: string;
export let tooltipContent =
$organization.billingPlan === BillingPlan.FREE
$organization?.billingPlan === BillingPlan.FREE
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization.billingPlan).name
tierToPlan($organization?.billingPlan)?.name
} plan`;
export let disabled: boolean;
export let buttonText: string;
export let buttonMethod: () => void | Promise<void>;
export let buttonHref: string = null;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let icon = 'plus';
export let showIcon = true;
@@ -31,7 +32,8 @@
secondary={buttonType === 'secondary'}
on:click={buttonMethod}
event={buttonEvent}
{disabled}>
{disabled}
href={buttonHref}>
{#if showIcon}
<span class={`icon-${icon}`} aria-hidden="true" />
{/if}
+3 -1
View File
@@ -31,6 +31,7 @@
export let buttonText: string = null;
export let buttonMethod: () => void = null;
export let buttonHref: string = null;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let buttonDisabled = false;
@@ -177,7 +178,8 @@
disabled={isButtonDisabled}
{buttonText}
{buttonEvent}
{buttonMethod} />
{buttonMethod}
{buttonHref} />
{/if}
</slot>
</header>
+16 -17
View File
@@ -1,8 +1,6 @@
<script>
import { settings } from '$lib/components/consent.svelte';
import { clickOnEnter } from '$lib/helpers/a11y';
import { isCloud } from '$lib/system';
import { version } from '$routes/console/store';
import { version } from '$routes/(console)/store';
const currentYear = new Date().getFullYear();
</script>
@@ -19,6 +17,7 @@
<a
class="text"
href="https://github.com/appwrite/appwrite/releases"
aria-label="Appwrite releases on Github"
target="_blank"
rel="noreferrer">
Version {$version}
@@ -43,14 +42,9 @@
</li>
{#if isCloud}
<li class="inline-links-item">
<span
style:cursor="pointer"
role="button"
tabindex="0"
on:keyup={clickOnEnter}
on:click={() => settings.set(true)}>
<a href="https://appwrite.io/cookies" target="_blank" rel="noreferrer">
<span class="text">Cookies</span>
</span>
</a>
</li>
{/if}
</ul>
@@ -61,14 +55,19 @@
<span class="text">{currentYear} Appwrite. All rights reserved.</span>
</li>
<li class="inline-links-item u-flex u-gap-8">
<a href="https://github.com/appwrite/appwrite" target="_blank" rel="noreferrer">
<span class="icon-github" aria-hidden="true" aria-label="Appwrite on Github" />
<a
href="https://github.com/appwrite/appwrite"
target="_blank"
rel="noreferrer"
aria-label="Appwrite on Github">
<span class="icon-github" aria-hidden="true" />
</a>
<a href="https://appwrite.io/discord" target="_blank" rel="noreferrer">
<span
class="icon-discord"
aria-hidden="true"
aria-label="Appwrite on Discord" />
<a
href="https://appwrite.io/discord"
target="_blank"
rel="noreferrer"
aria-label="Appwrite on Discord">
<span class="icon-discord" aria-hidden="true" />
</a>
</li>
</ul>

Some files were not shown because too many files have changed in this diff Show More