Merge branch 'main' into org-deletion-new-flow

This commit is contained in:
ItzNotABug
2024-10-03 12:49:42 +05:30
942 changed files with 19757 additions and 8125 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'
+39
View File
@@ -0,0 +1,39 @@
map $sent_http_content_type $expires {
# cache everything for 1 year
default 1y;
# html files shouldn't be cached for single-page applications
text/html off;
}
server {
listen 80;
server_name localhost;
# serve compressed file if filename.gz exists
gzip_static on;
location /console {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /console/index.html;
# Add cache headers
expires $expires;
add_header Pragma public;
add_header Cache-Control "public";
# Deny IE browsers from going into quirks mode
add_header X-UA-Compatible "IE=Edge";
# X-Frame-Options is to prevent from clickJacking attack
add_header X-Frame-Options SAMEORIGIN;
# This header enables the Cross-site scripting (XSS) filter
add_header X-XSS-Protection "1; mode=block;";
# disable content-type sniffing on some browsers.
add_header X-Content-Type-Options nosniff;
}
location / {
absolute_redirect off;
return 301 /console;
}
}
+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.31.0",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.14",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.9",
"deep-equal": "^2.2.2",
"dotenv": "^16.3.1",
"echarts": "^5.4.3",
"nanoid": "^4.0.2",
"plausible-tracker": "^0.3.8",
"dayjs": "^1.11.13",
"deep-equal": "^2.2.3",
"echarts": "^5.5.1",
"envfile": "^7.1.0",
"nanoid": "^5.0.7",
"plausible-tracker": "^0.3.9",
"pretty-bytes": "^6.1.1",
"prismjs": "^1.29.0",
"svelte-confetti": "^1.3.0",
"svelte-confetti": "^1.4.0",
"tippy.js": "^6.3.7"
},
"devDependencies": {
"@melt-ui/pp": "^0.1.4",
"@melt-ui/svelte": "^0.61.2",
"@playwright/test": "^1.44.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.3.4",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@testing-library/dom": "^9.0.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^4.0.3",
"@testing-library/user-event": "^14.4.3",
"@types/deep-equal": "^1.0.1",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"@vitest/ui": "^1.2.1",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-svelte": "^2.33.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.47.2",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.5.28",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/svelte": "^5.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/deep-equal": "^1.0.4",
"@types/prismjs": "^1.26.4",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/ui": "^1.6.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.0",
"jsdom": "^22.1.0",
"kleur": "^4.1.5",
"prettier": "^3.2.2",
"prettier-plugin-svelte": "^3.1.2",
"sass": "^1.66.1",
"svelte": "^4.2.9",
"svelte-check": "^3.5.1",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"sass": "^1.79.3",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^5.0.4",
"svelte-preprocess": "^6.0.2",
"svelte-sequential-preprocessor": "^2.0.1",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^5.0.11",
"vitest": "^1.2.1"
"tslib": "^2.7.0",
"typescript": "^5.6.2",
"vite": "^5.4.7",
"vitest": "^1.6.0"
},
"type": "module"
"type": "module",
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
}
+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: 'https://dlbilling.appwrite.org/v1',
PUBLIC_CONSOLE_MODE: 'cloud',
PUBLIC_STRIPE_KEY:
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
},
command: 'npm run build && npm run preview',
command: 'pnpm run build && pnpm run preview',
port: 4173
}
};
+7609
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();
+10 -1
View File
@@ -184,6 +184,7 @@ export enum Submit {
ProjectUpdateSMTP = 'submit_project_update_smtp',
MemberCreate = 'submit_member_create',
MemberDelete = 'submit_member_delete',
MembershipUpdate = 'submit_membership_update',
MembershipUpdateStatus = 'submit_membership_update_status',
ProviderUpdate = 'submit_provider_update',
TeamCreate = 'submit_team_create',
@@ -194,6 +195,8 @@ export enum Submit {
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
SessionsLengthUpdate = 'submit_sessions_length_update',
SessionsLimitUpdate = 'submit_sessions_limit_update',
SessionDelete = 'submit_session_delete',
@@ -226,13 +229,16 @@ export enum Submit {
FunctionUpdateLogging = 'submit_function_update_logging',
FunctionUpdateTimeout = 'submit_function_update_timeout',
FunctionUpdateEvents = 'submit_function_update_events',
FunctionUpdateScopes = 'submit_function_key_update_scopes',
FunctionConnectRepo = 'submit_function_connect_repo',
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
FunctionRedeploy = 'submit_function_redeploy',
DeploymentCreate = 'submit_deployment_create',
DeploymentDelete = 'submit_deployment_delete',
DeploymentUpdate = 'submit_deployment_update',
DeploymentCancel = 'submit_deployment_cancel',
ExecutionCreate = 'submit_execution_create',
ExecutionDelete = 'submit_execution_delete',
VariableCreate = 'submit_variable_create',
VariableDelete = 'submit_variable_delete',
VariableUpdate = 'submit_variable_update',
@@ -314,5 +320,8 @@ export enum Submit {
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete',
ApplyQuickFilter = 'submit_apply_quick_filter',
RequestBAA = 'submit_request_baa',
RequestSoc2 = 'submit_request_soc2'
}
+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);
@@ -18,7 +19,7 @@
let budgetEnabled = false;
$: currentPlan = $plansInfo.get(billingPlan);
$: extraSeatsCost = (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
$: extraSeatsCost = 0; // 0 untile trial period later replace (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
$: grossCost = currentPlan.price + extraSeatsCost;
$: estimatedTotal =
couponData?.status === 'active'
@@ -41,8 +42,8 @@
<p class="text">{formatCurrency(currentPlan.price)}</p>
</span>
<span class="u-flex u-main-space-between">
<p class="text">Additional seats ({collaborators?.length})</p>
<p class="text">
<p class="text" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
<p class="text" class:u-bold={isDowngrade}>
{formatCurrency(extraSeatsCost)}
</p>
</span>
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@@ -7,10 +8,18 @@
<div class="top-banner">
<div class="top-banner-bg">
<div class="top-banner-bg-1">
<img src="/images/top-banner/bg-pink-desktop.svg" width="1283" height="1278" alt="" />
<img
src={`${base}/images/top-banner/bg-pink-desktop.svg`}
width="1283"
height="1278"
alt="" />
</div>
<div class="top-banner-bg-2">
<img src="/images/top-banner/bg-mint-desktop.svg" width="1051" height="1271" alt="" />
<img
src={`${base}/images/top-banner/bg-mint-desktop.svg`}
width="1051"
height="1271"
alt="" />
</div>
</div>
<div class="top-banner-content u-color-text-primary">
+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}
+414
View File
@@ -0,0 +1,414 @@
<script lang="ts">
import { Button } from '$lib/elements/forms/index';
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { app } from '$lib/stores/app';
import {
type BottomModalAlertItem,
bottomModalAlerts,
dismissBottomModalAlert,
hideAllModalAlerts
} from '$lib/stores/bottom-alerts';
import { onMount } from 'svelte';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import { upgradeURL } from '$lib/stores/billing';
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
import { project } from '$routes/(console)/project-[project]/store';
import { page } from '$app/stores';
let currentIndex = 0;
let openModalOnMobile = false;
function getPageScope(pathname: string) {
const isProjectPage = pathname.includes('project-[project]');
const isOrganizationPage = pathname.includes('organization-[organization]');
return { isProjectPage, isOrganizationPage };
}
function filterModalAlerts(alerts: BottomModalAlertItem[], pathname: string) {
const { isProjectPage, isOrganizationPage } = getPageScope(pathname);
return alerts
.sort((a, b) => b.importance - a.importance)
.filter((alert) => {
return (
alert.show &&
shouldShowNotification(alert.id) &&
// if no scope > show in projects & org pages.
((!alert.scope && (isProjectPage || isOrganizationPage)) ||
// project scope, show only in project pages
(isProjectPage && alert.scope === 'project') ||
// organization scope, show only in organization pages
(isOrganizationPage && alert.scope === 'organization'))
);
});
}
$: filteredModalAlerts = filterModalAlerts($bottomModalAlerts, $page.route.id);
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
function handleClose() {
const modalAlert = currentModalAlert;
dismissBottomModalAlert(modalAlert.id);
hideNotification(modalAlert.id, { coolOffPeriod: 24 * 365 });
if (modalAlert.closed) modalAlert.closed();
if (currentIndex === filteredModalAlerts.length - 1 && filteredModalAlerts.length > 1) {
currentIndex = currentIndex - 1;
} else {
currentIndex = currentIndex % filteredModalAlerts.length;
}
}
function showNext() {
currentIndex = (currentIndex + 1) % filteredModalAlerts.length;
}
function showPrevious() {
currentIndex = (currentIndex - 1 + filteredModalAlerts.length) % filteredModalAlerts.length;
}
function showUpgrade() {
const plan = currentModalAlert.plan;
const organizationPlan = $organization.billingPlan;
switch (plan) {
case 'free':
return false;
case 'pro':
return organizationPlan === BillingPlan.FREE;
case 'scale':
return (
organizationPlan === BillingPlan.FREE || organizationPlan === BillingPlan.PRO
);
}
}
onMount(() => {
addBottomModalAlerts();
});
</script>
{#if filteredModalAlerts.length > 0 && currentModalAlert}
{@const shouldShowUpgrade = showUpgrade()}
<div class="main-alert-wrapper is-not-mobile">
<div class="alert-container">
<article class="card">
{#key currentModalAlert.id}
<button class="icon-inline-tag" on:click={() => handleClose()}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
fill="#97979B" />
</svg>
</button>
<div class="content-wrapper u-flex-vertical u-gap-16">
{#if $app.themeInUse === 'dark'}
<img
src={currentModalAlert.src.dark}
alt={currentModalAlert.title}
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
{:else}
<img
src={currentModalAlert.src.light}
alt={currentModalAlert.title}
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
{/if}
{#if filteredModalAlerts.length > 1}
<div class="u-flex u-main-space-between u-cross-baseline">
<span class="inline-tag feature-count-tag">
Feature {currentIndex + 1} of {filteredModalAlerts.length}
</span>
<div class="u-flex u-gap-10">
<button
class="icon-cheveron-left"
on:click={showPrevious}
disabled={currentIndex === 0}
class:active={currentIndex > 0} />
<button
class="icon-cheveron-right"
on:click={showNext}
disabled={currentIndex === filteredModalAlerts.length - 1}
class:active={currentIndex !==
filteredModalAlerts.length - 1} />
</div>
</div>
{/if}
<div class="u-flex-vertical u-gap-4 u-padding-inline-8">
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
<span class="u-width-fit-content">
{#if currentModalAlert.isHtml}
{@html currentModalAlert.message}
{:else}
{currentModalAlert.message}
{/if}
</span>
</div>
<div
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
<Button
secondary
class="button"
href={shouldShowUpgrade
? $upgradeURL
: currentModalAlert.cta.link({
organization: $organization,
project: $project
})}
external={!!currentModalAlert.cta.external}
fullWidthMobile>
{currentModalAlert.cta.text}
</Button>
{#if currentModalAlert.learnMore}
<Button
text
class="button"
external
fullWidthMobile
href={currentModalAlert.learnMore.link({
organization: $organization,
project: $project
})}>
{currentModalAlert.learnMore.text}
</Button>
{/if}
</div>
</div>
{/key}
</article>
</div>
</div>
<div class="main-alert-wrapper is-only-mobile" class:closed={!openModalOnMobile}>
{#if openModalOnMobile}
<div class="alert-container">
<article class="card">
{#key currentModalAlert.id}
<button class="icon-inline-tag" on:click={() => handleClose()}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
fill="#97979B" />
</svg>
</button>
<div class="content-wrapper u-flex-vertical u-gap-16">
{#if $app.themeInUse === 'dark'}
<img
src={currentModalAlert.src.dark}
alt={currentModalAlert.title}
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
{:else}
<img
src={currentModalAlert.src.light}
alt={currentModalAlert.title}
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
{/if}
{#if filteredModalAlerts.length > 1}
<div class="u-flex u-main-space-between u-cross-baseline">
<span class="inline-tag feature-count-tag">
Feature {currentIndex + 1} of {filteredModalAlerts.length}
</span>
<div class="u-flex u-gap-10">
<button
class="icon-cheveron-left"
on:click={showPrevious}
disabled={currentIndex === 0}
class:active={currentIndex > 0} />
<button
class="icon-cheveron-right"
on:click={showNext}
disabled={currentIndex ===
filteredModalAlerts.length - 1}
class:active={currentIndex !==
filteredModalAlerts.length - 1} />
</div>
</div>
{/if}
<div class="u-flex-vertical u-gap-8 u-padding-inline-8">
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
<span class="u-width-fit-content">
{#if currentModalAlert.isHtml}
{@html currentModalAlert.message}
{:else}
{currentModalAlert.message}
{/if}
</span>
</div>
<div
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
<Button
secondary
class="button"
href={shouldShowUpgrade
? $upgradeURL
: currentModalAlert.cta.link({
organization: $organization,
project: $project
})}
external={!!currentModalAlert.cta.external}
fullWidthMobile
on:click={() => (openModalOnMobile = false)}>
{shouldShowUpgrade
? 'Upgrade plan'
: currentModalAlert.cta.text}
</Button>
{#if currentModalAlert.learnMore}
<Button
text
class="button"
external
fullWidthMobile
on:click={() => (openModalOnMobile = false)}
href={currentModalAlert.learnMore.link({
organization: $organization,
project: $project
})}>
{currentModalAlert.learnMore.text}
</Button>
{/if}
</div>
</div>
{/key}
</article>
</div>
{:else}
<button
class:showing={!openModalOnMobile}
class="card notification-card u-width-full-line"
on:click={() => (openModalOnMobile = true)}>
<div class="u-flex-vertical u-gap-4">
<div class="u-flex u-cross-center u-main-space-between">
<h3 class="body-text-2 u-bold">New features available</h3>
<button on:click={hideAllModalAlerts}>
<span class="icon-x" />
</button>
</div>
<span class="u-width-fit-content">
Explore new features to enhance your projects and improve security.
</span>
</div>
</button>
{/if}
</div>
{/if}
<style>
.card {
padding: 0.5rem;
}
.main-alert-wrapper {
left: 1rem;
z-index: 25;
bottom: 1rem;
position: fixed;
max-width: 289px;
}
.feature-count-tag {
font-size: 12px;
font-weight: 400;
width: fit-content;
margin-inline-start: 0.5rem;
}
.icon-inline-tag {
top: 1rem;
right: 1rem;
background: #fff;
position: absolute;
display: inline-flex;
padding: var(--space-2, 4px);
border-radius: var(--border-radius-S, 8px);
border: hsl(var(--color-neutral-10)) solid 1px;
}
:global(.theme-dark) .icon-inline-tag {
background: #1d1d21;
border: hsl(var(--color-neutral-80)) solid 1px;
}
.u-gap-10 {
gap: 0.625rem;
}
.icon-cheveron-left,
.icon-cheveron-right {
opacity: 0.5;
color: #97979b;
width: var(--icon-size-M, 20px);
height: var(--icon-size-M, 20px);
}
.active {
opacity: 1;
}
@media (max-width: 768px) {
.main-alert-wrapper {
top: 50%;
left: 50%;
display: flex;
min-width: 100%;
min-height: 100%;
max-width: 100vw;
align-items: center;
justify-content: center;
backdrop-filter: blur(6px);
transform: translate(-50%, -50%);
}
.main-alert-wrapper.closed {
backdrop-filter: unset;
}
.notification-card {
padding: var(--space-5, 10px) var(--space-6, 12px);
}
.main-alert-wrapper:has(.card.notification-card) {
bottom: 0;
top: unset;
min-height: auto;
padding-inline: 0.5rem;
}
.alert-container {
max-width: 90vw;
}
}
</style>
+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>
+2 -1
View File
@@ -7,6 +7,7 @@
import { isCloud } from '$lib/system';
import CardPlanLimit from './cardPlanLimit.svelte';
export let showEmpty = true;
export let offset = 0;
export let total = 0;
export let event: string = null;
@@ -27,7 +28,7 @@
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
{#if isCloud && serviceId && total >= planLimit}
<CardPlanLimit {service} />
{:else}
{:else if showEmpty}
<Empty on:click target={event}>
<slot name="empty" />
</Empty>
@@ -57,6 +57,9 @@
<style lang="scss">
// TODO: remove once pink is updated
.collapsible-item {
.collapsible-wrapper {
padding-left: 0;
}
.collapsible-wrapper.is-disabled {
cursor: not-allowed;
+1 -1
View File
@@ -8,7 +8,7 @@
</script>
<div class:box={isBox}>
<div class="u-flex u-main-space-between u-cross-start">
<div class="u-flex u-main-space-between u-cross-start" style="padding-block: 0.5rem;">
<div class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2">
<span class="u-flex u-cross-center u-gap-8">
<p class="text u-bold">
+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}>
+22 -6
View File
@@ -5,14 +5,28 @@
import EmptyDark from '$lib/images/empty-dark.svg';
import { Heading } from '.';
import { trackEvent } from '$lib/actions/analytics';
import { createEventDispatcher } from 'svelte';
export let single = false;
export let noMedia = false;
export let target: string = null;
export let href: string = null;
export let marginTop = false;
export let allowCreate = true;
const dispatch = createEventDispatcher();
function onClick(event) {
if (!allowCreate) {
return;
}
dispatch('click', event);
}
function track() {
if (!allowCreate) {
return;
}
if (target) {
trackEvent(`click_create_${target}`, {
from: 'empty'
@@ -28,13 +42,13 @@
{#if !noMedia}
<button
type="button"
on:click|preventDefault
on:click={track}
on:click={onClick}
aria-label="create {target}">
{#if $app.themeInUse === 'dark'}
<img src={EmptyDark} alt="create" aria-hidden="true" width="376" />
<img src={EmptyDark} alt="create" aria-hidden="true" height="242" />
{:else}
<img src={EmptyLight} alt="create" aria-hidden="true" width="376" />
<img src={EmptyLight} alt="create" aria-hidden="true" height="242" />
{/if}
</button>
{/if}
@@ -54,9 +68,11 @@
text
event="empty_documentation"
ariaLabel="create {target}">Documentation</Button>
<Button secondary on:click on:click={track}>
Create {target}
</Button>
{#if allowCreate}
<Button secondary on:click on:click={track}>
Create {target}
</Button>
{/if}
</div>
</slot>
</div>
+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>
+8 -2
View File
@@ -6,7 +6,10 @@
feedbackOptions,
feedbackData
} from '$lib/stores/feedback';
import { user } from '$lib/stores/user';
import { organization } from '$lib/stores/organization';
import { addNotification } from '$lib/stores/notifications';
import { page } from '$app/stores';
$: $selectedFeedback = feedbackOptions.find((option) => option.type === $feedback.type);
@@ -15,8 +18,11 @@
await feedback.submitFeedback(
`feedback-${$feedback.type}`,
$feedbackData.message,
$feedbackData.name,
$feedbackData.email
$user.name,
$user.email,
$organization.billingPlan,
$page.url.href,
$feedbackData.value
);
addNotification({
type: 'success',
@@ -1,15 +1,9 @@
<script lang="ts">
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
import { FormList, InputTextarea } from '$lib/elements/forms';
import { feedbackData } from '$lib/stores/feedback';
</script>
<FormList>
<InputText label="Name" id="name" bind:value={$feedbackData.name} placeholder="Enter name" />
<InputEmail
label="Email"
id="email"
bind:value={$feedbackData.email}
placeholder="Enter email" />
<InputTextarea
id="feedback"
placeholder="Your message here"
+1 -11
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { FormList, InputTextarea, InputText, InputEmail } from '$lib/elements/forms';
import { FormList, InputTextarea } from '$lib/elements/forms';
import { feedbackData } from '$lib/stores/feedback';
import Evaluation from './evaluation.svelte';
</script>
@@ -16,15 +16,5 @@
required
bind:value={$feedbackData.message}
showLabel={false} />
<InputText
label="Name"
id="name"
bind:value={$feedbackData.name}
placeholder="Enter name" />
<InputEmail
label="Email"
id="email"
bind:value={$feedbackData.email}
placeholder="Enter email" />
{/if}
</FormList>
+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 />
+2 -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';
@@ -75,3 +75,4 @@ export { default as ModalSideCol } from './modalSideCol.svelte';
export { default as EmptyCardImageCloud } from './emptyCardImageCloud.svelte';
export { default as ImagePreview } from './imagePreview.svelte';
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
export { default as BottomModalAlert } from './bottomModalAlert.svelte';
+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}
+20 -3
View File
@@ -1,4 +1,6 @@
<script context="module" lang="ts">
let inputDigitFields: InputDigits;
export async function verify(challenge: Models.MfaChallenge, code: string) {
try {
if (challenge == null) {
@@ -10,6 +12,7 @@
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountCreate);
} catch (error) {
inputDigitFields?.clearInputsAndRefocus();
trackError(error, Submit.AccountCreate);
throw error;
}
@@ -24,6 +27,7 @@
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
import { addNotification } from '$lib/stores/notifications';
export let factors: Models.MfaFactors & { recoveryCode: boolean };
/** If true, the form will be submitted automatically when the code is entered. */
@@ -40,8 +44,16 @@
async function createChallenge(factor: AuthenticationFactor) {
disabled = true;
challengeType = factor;
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
disabled = false;
try {
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
} finally {
disabled = false;
}
}
onMount(async () => {
@@ -72,7 +84,12 @@
{:else if challengeType == AuthenticationFactor.Phone}
<p>A 6-digit verification code was sent to your phone, enter it below.</p>
{/if}
<InputDigits bind:value={code} required autofocus {autoSubmit} />
<InputDigits
bind:value={code}
required
autofocus
{autoSubmit}
bind:this={inputDigitFields} />
{/if}
{#if showVerifyButton}
<FormItem>
+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}
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<p>
<span class="heading-level-4">{currentValue}</span>
<span class="body-text-1 u-bold">{currentUnit}</span>
</p>
<p class="heading-level-4">{progress}%</p>
</div>
<p class="body-text-2">
{maxValue}
{maxUnit ? maxUnit : ''}
</p>
<p class="heading-level-4">{progress}%</p>
</div>
<p class="body-text-2">
{maxValue}
{maxUnit}
</p>
</div>
{#if showBar}
<div
class="progress-bar-container u-margin-block-start-16"
class:is-warning={progress >= 75 && progress < 100}
class:is-danger={progress >= 100}
style:--graph-size={Math.max(Math.min(progress, maximum), minimum) + '%'} />
{/if}
{#if showBar && progressBarData.length > 0}
<ProgressBar maxSize={progressMax} data={progressBarData} />
{/if}
</section>
@@ -0,0 +1,93 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import type { ProgressbarData, ProgressbarProps } from '$lib/components';
type $$Props = ProgressbarProps;
/**
* The max value of the progressbar
*/
export let maxSize: $$Props['maxSize'];
/**
* The data for the progressbar
*/
export let data: $$Props['data'];
/**
* The remaining value of the progressbar
*/
$: remainder = data.reduce((sum: number, item: ProgressbarData) => sum - item.size, maxSize);
</script>
<section class="progressbar__container">
{#each $$props.data as item}
<div
class="progressbar__content"
style:background-color={item.color}
style:width={`${(item.size / maxSize) * 100}%`}
use:tooltip={{
disabled: !item.tooltip,
allowHTML: true,
content: `<span class="u-bold">${item.tooltip.title}</span> ${item.tooltip.label}`
}}>
</div>
{/each}
{#if remainder > 0}
<div class="progressbar__content" style:width={`${(remainder / maxSize) * 100}%`} />
{/if}
</section>
<style lang="scss">
:root {
--progressbar-border-radius: 0.25rem;
--progressbar-tooltip-label-color: #818186;
--progressbar-tooltip-link-color: #6c6c71;
}
:global(.theme-dark) {
--progressbar-background-color: var(--neutral-800, #2d2d31);
--progressbar-tooltip-background-color: var(--neutral-800, #2d2d31);
--progressbar-tooltip-border-color: var(--neutral-80, #424248);
}
:global(.theme-light) {
--progressbar-background-color: var(--neutral-40, #f4f4f7);
--progressbar-tooltip-background-color: #ffffff;
--progressbar-tooltip-border-color: #ededf0;
}
.progressbar {
&__container {
height: 0.5rem;
background-color: var(--progressbar-background-color);
border-radius: var(--progressbar-border-radius);
display: flex;
flex-direction: row;
gap: 2px;
margin-top: 1rem;
}
&__content {
height: 100%;
min-width: 4px;
display: flex;
justify-content: center;
&::before {
content: '';
height: 2.5rem;
margin-top: -1.25rem;
width: 100%;
}
&:first-child {
border-top-left-radius: var(--progressbar-border-radius);
border-bottom-left-radius: var(--progressbar-border-radius);
}
&:last-child {
border-top-right-radius: var(--progressbar-border-radius);
border-bottom-right-radius: var(--progressbar-border-radius);
}
}
}
</style>
+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';
+21
View File
@@ -0,0 +1,21 @@
<div class="u-flex-vertical u-gap-16">
<slot />
</div>
<style>
div {
color: hsl(var(--color-neutral-50));
line-height: 1.25rem;
&:first-child {
color: hsl(var(--color-neutral-70));
}
}
:global(.theme-dark) div {
color: hsl(var(--color-neutral-20));
&:first-child {
color: hsl(var(--color-neutral-10));
}
}
</style>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
import Base from './base.svelte';
</script>
<Base>
<div class="u-flex-vertical u-gap-8">
<p>
By default, all members are assigned a <span class="u-bold">Developer</span> role. You can
change this at any time from your organization settings.
</p>
<p>
<a
class="link"
target="_blank"
rel="noopener noreferrer"
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</a> about roles.
</p>
</div>
</Base>
+17
View File
@@ -0,0 +1,17 @@
<script>
import Base from './base.svelte';
</script>
<Base>
<div class="u-flex-vertical u-gap-8">
<p class="u-bold">Roles</p>
<p>Owner, Developer, Editor, Analyst and Billing.</p>
<p>
<a
class="link"
target="_blank"
rel="noopener noreferrer"
href="https://appwrite.io/docs/advanced/platform/roles">Learn more</a> about roles.
</p>
</div>
</Base>
+62
View File
@@ -0,0 +1,62 @@
<script>
import Base from './base.svelte';
import { upgradeURL } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import Button from '$lib/elements/forms/button.svelte';
</script>
<Base>
{#if isCloud}
{#if $organization?.billingPlan !== BillingPlan.FREE}
<div class="u-flex-vertical u-gap-8">
<p>
<span class="u-bold">Roles</span>
{#if $organization?.billingPlan === BillingPlan.FREE}
<span class="inline-tag u-normal u-x-small">Pro plan</span>
{/if}
</p>
<p>Owner, Developer, Editor, Analyst and Billing.</p>
</div>
<p>
<Button link external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button> about roles.
</p>
{:else}
<div class="u-flex-vertical u-gap-8">
<p>
<span class="u-bold">Roles</span>
{#if $organization?.billingPlan === BillingPlan.FREE}
<span class="inline-tag u-normal u-x-small">Pro plan</span>
{/if}
</p>
<p>
Upgrade to Pro to assign new roles to members such as Owner, Developer, Editor
or Analyst.
</p>
</div>
<p class="u-flex u-main-end u-cross-center u-gap-4">
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button>
<Button secondary external href={$upgradeURL}>Upgrade plan</Button>
</p>
{/if}
{:else}
<div class="u-flex-vertical u-gap-8">
<p>
<span class="u-bold">Roles</span>
<span class="inline-tag u-normal u-x-small">Appwrite Cloud</span>
</p>
<p>
Upgrade to Cloud to assign new roles to members or ask us about our entriprise self
hosted offering.
</p>
</div>
<p class="u-flex u-main-end u-cross-center u-gap-4">
<Button text external href="https://appwrite.io/docs/advanced/platform/roles"
>Learn more</Button>
<Button secondary external href={$upgradeURL}>Upgrade to Cloud</Button>
</p>
{/if}
</Base>
+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">
+136 -38
View File
@@ -61,204 +61,302 @@ export enum Dependencies {
MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
}
export const defaultScopes: string[] = [
'global',
'public',
'home',
'console',
'graphql',
'sessions.write',
'account',
'teams.read',
'teams.write',
'documents.read',
'documents.write',
'files.read',
'files.write',
'projects.read',
'projects.write',
'locale.read',
'avatars.read',
'execution.read',
'execution.write',
'targets.read',
'targets.write',
'subscribers.write',
'subscribers.read',
'assistant.read',
'users.read',
'users.write',
'databases.read',
'databases.write',
'collections.read',
'collections.write',
'buckets.read',
'buckets.write',
'functions.read',
'functions.write',
'platforms.read',
'platforms.write',
'keys.read',
'keys.write',
'webhooks.read',
'webhooks.write',
'rules.read',
'rules.write',
'migrations.read',
'migrations.write',
'vcs.read',
'vcs.write',
'providers.read',
'providers.write',
'messages.read',
'messages.write',
'topics.read',
'topics.write',
'billing.read',
'billing.write'
];
export const defaultRoles: string[] = ['owner'];
export const scopes: {
scope: string;
description: string;
category: string;
icon: string;
}[] = [
{
scope: 'sessions.write',
description: "Access to create, update and delete your project's sessions",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'users.read',
description: "Access to read your project's users",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'users.write',
description: "Access to create, update, and delete your project's users",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'teams.read',
description: "Access to read your project's teams",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'teams.write',
description: "Access to create, update, and delete your project's teams",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'databases.read',
description: "Access to read your project's databases",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'databases.write',
description: "Access to create, update, and delete your project's databases",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'collections.read',
description: "Access to read your project's database collections",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'collections.write',
description: "Access to create, update, and delete your project's database collections",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.write',
description:
"Access to create, update, and delete your project's database collection's attributes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.read',
description: "Access to read your project's database collection's indexes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.write',
description:
"Access to create, update, and delete your project's database collection's indexes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'documents.read',
description: "Access to read your project's database documents",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'documents.write',
description: "Access to create, update, and delete your project's database documents",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'files.read',
description: "Access to read your project's storage files and preview images",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'files.write',
description: "Access to create, update, and delete your project's storage files",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'buckets.read',
description: "Access to read your project's storage buckets",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'buckets.write',
description: "Access to create, update, and delete your project's storage buckets",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'functions.read',
description: "Access to read your project's functions and code deployments",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'functions.write',
description:
"Access to create, update, and delete your project's functions and code deployments",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'execution.read',
description: "Access to read your project's execution logs",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'execution.write',
description: "Access to execute your project's functions",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'targets.read',
description: "Access to read your project's messaging targets",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'targets.write',
description: "Access to create, update, and delete your project's messaging targets",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'providers.read',
description: "Access to read your project's messaging providers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'providers.write',
description: "Access to create, update, and delete your project's messaging providers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'messages.read',
description: "Access to read your project's messages",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'messages.write',
description: "Access to create, update, and delete your project's messages",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'topics.read',
description: "Access to read your project's messaging topics",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'topics.write',
description: "Access to create, update, and delete your project's messaging topics",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'subscribers.read',
description: "Access to read your project's messaging topic subscribers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'subscribers.write',
description:
"Access to create, update, and delete your project's messaging topic subscribers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'locale.read',
description: "Access to access your project's Locale service",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'avatars.read',
description: "Access to access your project's Avatars service",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'health.read',
description: "Access to read your project's health status",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'migrations.read',
description: "Access to read your project's migration status",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'migrations.write',
description: 'Access to create migrations',
category: 'Other'
category: 'Other',
icon: 'globe'
}
];
+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"
+15
View File
@@ -38,6 +38,21 @@
}
});
/**
* Clears the input fields and moves the focus to the first input.
* Usually used when resetting fields on auth fails, etc.
*/
export function clearInputsAndRefocus() {
value = '';
autoSubmitted = false;
if (element) {
const inputs = element.querySelectorAll('input');
inputs.forEach((input) => (input.value = ''));
if (autofocus) inputs[0].focus();
}
}
onMount(() => {
const interval = setInterval(() => {
const input = element.querySelector('input');
+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>
+31 -1
View File
@@ -1,9 +1,12 @@
<script lang="ts">
import { FormItem, FormItemPart, Helper, Label } from '.';
import type { FormItemTag } from './formItem.svelte';
import { Drop } from '$lib/components';
import type { SvelteComponent } from 'svelte';
export let id: string;
export let label: string | undefined = undefined;
export let ariaLabel = label;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let value: string | number | boolean | null;
@@ -18,10 +21,14 @@
}[];
export let isMultiple = false;
export let fullWidth = false;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
let element: HTMLSelectElement;
let error: string;
let show: boolean = false;
const handleInvalid = (event: Event) => {
event.preventDefault();
@@ -53,7 +60,29 @@
<svelte:component this={wrapper} {fullWidth} tag={wrapperTag}>
{#if label}
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id}>
{label}
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
&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:--card-border-radius="var(--border-radius-small)"
style:--p-card-padding=".75rem"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
@@ -62,6 +91,7 @@
{id}
{required}
{disabled}
aria-label={ariaLabel}
bind:this={element}
bind:value
on:invalid={handleInvalid}

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