mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' of https://github.com/appwrite/console into mock-phone-numbers
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
name: E2E
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: ['**']
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
|
||||
env:
|
||||
VITE_STRIPE_PUBLIC_KEY: ${{ vars.VITE_STRIPE_PUBLIC_KEY }}
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: E2E Tests
|
||||
run: npm run e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Mark stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Midnight Runtime
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: "This issue has been labeled as a 'question', indicating that it requires additional information from the requestor. It has been inactive for 7 days. If no further activity occurs, this issue will be closed in 14 days."
|
||||
stale-issue-label: 'stale'
|
||||
days-before-stale: 7
|
||||
days-before-close: 14
|
||||
remove-stale-when-updated: true
|
||||
close-issue-message: 'This issue has been closed due to inactivity. If you still require assistance, please provide the requested information.'
|
||||
close-issue-reason: 'not_planned'
|
||||
operations-per-run: 100
|
||||
only-labels: 'question'
|
||||
@@ -1,13 +1,8 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: ['**']
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
@@ -23,9 +18,9 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
# - name: Audit dependencies
|
||||
# run: npm audit --audit-level low
|
||||
node-version: 20
|
||||
- name: Audit dependencies
|
||||
run: npm audit --audit-level critical
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Svelte Diagnostics
|
||||
@@ -33,6 +28,6 @@ jobs:
|
||||
- name: Linter
|
||||
run: npm run lint
|
||||
- name: Unit Tests
|
||||
run: npm test
|
||||
run: npm run test
|
||||
- name: Build Console
|
||||
run: npm run build
|
||||
|
||||
+3
-1
@@ -8,6 +8,8 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
.DS_STORE
|
||||
.cache
|
||||
@@ -145,4 +147,4 @@ dist
|
||||
.stylelintcache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
.svelte-kit
|
||||
|
||||
Generated
+1250
-1245
File diff suppressed because it is too large
Load Diff
+13
-14
@@ -12,38 +12,37 @@
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:watch": "vitest watch",
|
||||
"e2e": "playwright test tests/e2e"
|
||||
"test": "TZ=EST vitest run",
|
||||
"test:ui": "TZ=EST vitest --ui",
|
||||
"test:watch": "TZ=EST vitest watch",
|
||||
"e2e": "playwright test tests/e2e",
|
||||
"e2e:ui": "playwright test tests/e2e --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.5.0",
|
||||
"@appwrite.io/pink": "0.3.0",
|
||||
"@appwrite.io/pink-icons": "0.3.0",
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/pink": "0.23.0",
|
||||
"@appwrite.io/pink-icons": "0.23.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
"@stripe/stripe-js": "^2.2.0",
|
||||
"@stripe/stripe-js": "^3.4.0",
|
||||
"ai": "^2.2.11",
|
||||
"analytics": "^0.8.9",
|
||||
"dayjs": "^1.11.9",
|
||||
"deep-equal": "^2.2.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"echarts": "^5.4.3",
|
||||
"logrocket": "^5.0.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"svelte-confetti": "^1.3.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"web-vitals": "^3.4.0"
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.1.4",
|
||||
"@melt-ui/svelte": "^0.61.2",
|
||||
"@playwright/test": "^1.37.1",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.3.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
@@ -53,8 +52,8 @@
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.5.0",
|
||||
"@typescript-eslint/parser": "^6.5.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",
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { type PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
timeout: 120000,
|
||||
reportSlowTests: null,
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
webServer: {
|
||||
timeout: 120000,
|
||||
env: {
|
||||
VITE_APPWRITE_ENDPOINT: 'http://console-tests.appwrite.org/v1',
|
||||
VITE_CONSOLE_MODE: 'cloud'
|
||||
},
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
|
||||
<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="preload"
|
||||
href="/fonts/inter/inter-v8-latin-600.woff2"
|
||||
|
||||
Vendored
+1
-3
@@ -1,4 +1,2 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
interface Window {
|
||||
VERCEL_ANALYTICS_ID: string | false;
|
||||
}
|
||||
interface Window {}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppwriteException } from '@appwrite.io/console';
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
|
||||
export const handleError: HandleClientError = async ({ error, message, status }) => {
|
||||
if (error instanceof AppwriteException) {
|
||||
status = error.code === 0 ? undefined : error.code;
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
status
|
||||
};
|
||||
};
|
||||
@@ -151,6 +151,13 @@ export enum Submit {
|
||||
AccountDelete = 'submit_account_delete',
|
||||
AccountDeleteSession = 'submit_account_delete_session',
|
||||
AccountDeleteAllSessions = 'submit_account_delete_all_sessions',
|
||||
AccountUpdateMfa = 'submit_account_update_mfa',
|
||||
AccountAuthenticatorCreate = 'submit_account_authenticator_create',
|
||||
AccountAuthenticatorUpdate = 'submit_account_authenticator_update',
|
||||
AccountAuthenticatorDelete = 'submit_account_authenticator_delete',
|
||||
AccountRecoveryCodesCreate = 'submit_account_recovery_codes_create',
|
||||
AccountRecoveryCodesUpdate = 'submit_account_recovery_codes_update',
|
||||
AccountDeleteIdentity = 'submit_account_delete_identity',
|
||||
UserCreate = 'submit_user_create',
|
||||
UserDelete = 'submit_user_delete',
|
||||
UserUpdateEmail = 'submit_user_update_email',
|
||||
@@ -162,6 +169,10 @@ export enum Submit {
|
||||
UserUpdateStatus = 'submit_user_update_status',
|
||||
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
|
||||
UserUpdateVerificationPhone = 'submit_user_update_verification_phone',
|
||||
UserIdentityDelete = 'submit_user_identity_delete',
|
||||
UserTargetCreate = 'submit_user_target_create',
|
||||
UserTargetDelete = 'submit_user_target_delete',
|
||||
UserAuthenticatorDelete = 'submit_user_authenticator_delete',
|
||||
OrganizationCreate = 'submit_organization_create',
|
||||
OrganizationDelete = 'submit_organization_delete',
|
||||
OrganizationUpdateName = 'submit_organization_update_name',
|
||||
@@ -244,6 +255,7 @@ export enum Submit {
|
||||
WebhookUpdateUrl = 'submit_webhook_update_url',
|
||||
WebhookUpdateEvents = 'submit_webhook_update_events',
|
||||
WebhookUpdateName = 'submit_webhook_update_name',
|
||||
WebhookUpdateEnabled = 'submit_webhook_update_enabled',
|
||||
WebhookUpdateSecurity = 'submit_webhook_update_security',
|
||||
BucketCreate = 'submit_bucket_create',
|
||||
BucketDelete = 'submit_bucket_delete',
|
||||
@@ -265,6 +277,7 @@ export enum Submit {
|
||||
PaymentMethodUpdate = 'submit_payment_method_update',
|
||||
PaymentMethodDelete = 'submit_payment_method_delete',
|
||||
RetryPayment = 'submit_retry_payment',
|
||||
VerifyPayment = 'submit_verify_payment',
|
||||
BillingAddressCreate = 'submit_billing_address_create',
|
||||
BillingAddressUpdate = 'submit_billing_address_update',
|
||||
BillingAddressDelete = 'submit_billing_address_delete',
|
||||
@@ -290,5 +303,17 @@ export enum Submit {
|
||||
SmsResetTemplate = 'submit_sms_reset_template',
|
||||
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
|
||||
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template',
|
||||
MessagingProviderCreate = 'submit_messaging_provider_create',
|
||||
MessagingProviderDelete = 'submit_messaging_provider_delete',
|
||||
MessagingProviderUpdate = 'submit_messaging_provider_update',
|
||||
MessagingMessageCreate = 'submit_messaging_message_create',
|
||||
MessagingMessageUpdate = 'submit_messaging_message_update',
|
||||
MessagingMessageDelete = 'submit_messaging_message_delete',
|
||||
MessagingTopicCreate = 'submit_messaging_topic_create',
|
||||
MessagingTopicDelete = 'submit_messaging_topic_delete',
|
||||
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
|
||||
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
|
||||
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { onMount } from 'svelte';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { omit } from '$lib/helpers/omit';
|
||||
|
||||
const groups = [
|
||||
'ungrouped',
|
||||
@@ -17,6 +18,10 @@ const groups = [
|
||||
'platforms',
|
||||
'databases',
|
||||
'functions',
|
||||
'messaging',
|
||||
'messages',
|
||||
'providers',
|
||||
'topics',
|
||||
'storage',
|
||||
'domains',
|
||||
'webhooks',
|
||||
@@ -44,6 +49,7 @@ type BaseCommand = {
|
||||
forceEnable?: boolean;
|
||||
group?: CommandGroup;
|
||||
icon?: string;
|
||||
image?: string;
|
||||
rank?: number;
|
||||
nested?: boolean;
|
||||
keepOpen?: boolean;
|
||||
@@ -69,7 +75,23 @@ export const disabledMap = writable<Map<string, boolean>>(new Map());
|
||||
|
||||
// Derived stores
|
||||
export const commands = derived(commandMap, ($commandMap) => {
|
||||
return Array.from($commandMap.values()).flat();
|
||||
const res: Command[] = [];
|
||||
const keys = new Set<string>();
|
||||
|
||||
const allCommands = Array.from($commandMap.values()).flat().toReversed();
|
||||
|
||||
for (const command of allCommands) {
|
||||
if (isKeyedCommand(command) && !command.disabled) {
|
||||
const keysString = command.keys.join('+');
|
||||
if (keys.has(keysString)) {
|
||||
res.push(omit(command, 'keys'));
|
||||
continue;
|
||||
}
|
||||
keys.add(keysString);
|
||||
}
|
||||
res.push(command);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
const commandsEnabled = derived(disabledMap, ($disabledMap) => {
|
||||
@@ -109,9 +131,9 @@ function hasDisputing(command: KeyedCommand, allCommands: Command[]) {
|
||||
}
|
||||
|
||||
export const commandCenterKeyDownHandler = derived(
|
||||
[commandMap, commandsEnabled, wizard],
|
||||
([$commandMap, enabled, $wizard]) => {
|
||||
const commandsArr = Array.from($commandMap.values()).flat();
|
||||
[commands, commandsEnabled, wizard],
|
||||
([$commands, enabled, $wizard]) => {
|
||||
const commandsArr = $commands;
|
||||
let recentKeyCodes: number[] = [];
|
||||
let validCommands: KeyedCommand[] = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { providers } from '$routes/console/project-[project]/messaging/providers/store';
|
||||
import {
|
||||
messageParams,
|
||||
providerType,
|
||||
targetsById
|
||||
} 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';
|
||||
|
||||
let search = '';
|
||||
|
||||
let options = Object.entries(providers).map(([type, option]) => {
|
||||
return {
|
||||
label: option.name,
|
||||
icon: option.icon,
|
||||
callback() {
|
||||
if (
|
||||
type !== MessagingProviderType.Email &&
|
||||
type !== MessagingProviderType.Sms &&
|
||||
type !== MessagingProviderType.Push
|
||||
)
|
||||
return;
|
||||
$providerType = type;
|
||||
$topicsById = {};
|
||||
$targetsById = {};
|
||||
const common = {
|
||||
topics: [],
|
||||
users: [],
|
||||
targets: []
|
||||
};
|
||||
switch (type) {
|
||||
case MessagingProviderType.Email:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
subject: '',
|
||||
content: ''
|
||||
};
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
content: ''
|
||||
};
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
title: '',
|
||||
body: '',
|
||||
data: [['', '']]
|
||||
};
|
||||
break;
|
||||
}
|
||||
wizard.start(Create);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$: filteredOptions = options.filter((option) => {
|
||||
return option.label.toLowerCase().includes(search.toLowerCase());
|
||||
});
|
||||
</script>
|
||||
|
||||
<Template options={filteredOptions} bind:search>
|
||||
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
|
||||
<i class="icon-{option.icon}" />
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</Template>
|
||||
@@ -78,3 +78,9 @@ export const FilesPanel: SubPanel = {
|
||||
name: 'Files',
|
||||
component: Files
|
||||
};
|
||||
|
||||
import CreateMessage from './createMessage.svelte';
|
||||
export const CreateMessagePanel: SubPanel = {
|
||||
name: 'Create Message',
|
||||
component: CreateMessage
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
This is the root command panel. It precedes all other command panels.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
import { commands, searchers, type Command, isKeyedCommand } from '../commands';
|
||||
@@ -54,7 +56,13 @@
|
||||
<Template options={results} bind:search searchPlaceholder="Search for commands or content...">
|
||||
<div slot="option" class="u-flex u-main-space-between content" let:option={command}>
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
|
||||
{#if command.image}
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${command.image}.svg`}
|
||||
alt={command.label} />
|
||||
{:else}
|
||||
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
|
||||
{/if}
|
||||
<span>
|
||||
{command.label}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { goto } from '$app/navigation';
|
||||
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';
|
||||
|
||||
const getLabel = (message) => {
|
||||
switch (message.providerType) {
|
||||
case MessagingProviderType.Push:
|
||||
return message.data.title;
|
||||
case MessagingProviderType.Sms:
|
||||
return message.data.content;
|
||||
case MessagingProviderType.Email:
|
||||
return message.data.subject;
|
||||
default:
|
||||
return 'null';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = (message) => {
|
||||
switch (message.providerType) {
|
||||
case MessagingProviderType.Push:
|
||||
return 'device-mobile';
|
||||
case MessagingProviderType.Sms:
|
||||
return 'annotation';
|
||||
case MessagingProviderType.Email:
|
||||
return 'mail';
|
||||
default:
|
||||
return 'send';
|
||||
}
|
||||
};
|
||||
|
||||
export const messagesSearcher = (async (query: string) => {
|
||||
const { messages } = await sdk.forProject.messaging.listMessages([], query || undefined);
|
||||
|
||||
const projectId = get(project).$id;
|
||||
|
||||
return messages
|
||||
.filter((message) => getLabel(message).toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(message) =>
|
||||
({
|
||||
group: 'messages',
|
||||
label: getLabel(message),
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/messaging/message-${message.$id}`);
|
||||
},
|
||||
icon: getIcon(message)
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { goto } from '$app/navigation';
|
||||
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';
|
||||
|
||||
const getIcon = (provider: string) => {
|
||||
const { icon } = getProviderDisplayNameAndIcon(provider);
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const providersSearcher = (async (query: string) => {
|
||||
const { providers } = await sdk.forProject.messaging.listProviders([], query || undefined);
|
||||
|
||||
const projectId = get(project).$id;
|
||||
|
||||
return providers
|
||||
.filter((provider) => provider.name.toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(provider) =>
|
||||
({
|
||||
group: 'providers',
|
||||
label: provider.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`/console/project-${projectId}/messaging/providers/provider-${provider.$id}`
|
||||
);
|
||||
},
|
||||
image: getIcon(provider.provider)
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export const topicsSearcher = (async (query: string) => {
|
||||
const { topics } = await sdk.forProject.messaging.listTopics([], query || undefined);
|
||||
|
||||
const projectId = get(project).$id;
|
||||
|
||||
return topics
|
||||
.filter((topic) => topic.name.toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(topic) =>
|
||||
({
|
||||
group: 'topics',
|
||||
label: topic.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/messaging/topics/topic-${topic.$id}`);
|
||||
},
|
||||
icon: 'send'
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Buttons } from '../stores/notifications';
|
||||
import { createEventDispatcher, type ComponentProps } from 'svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
export let dismissible = false;
|
||||
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
|
||||
export let buttons: Buttons[] = [];
|
||||
export let buttons: (ComponentProps<Button> & {
|
||||
slot: string;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
})[] = [];
|
||||
export let isAction = false;
|
||||
export let isStandalone = false;
|
||||
let classes = '';
|
||||
@@ -27,7 +29,6 @@
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="close alert box"
|
||||
on:click={() => dispatch('dismiss')}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
@@ -54,8 +55,8 @@
|
||||
<div class="alert-buttons u-flex">
|
||||
<slot name="buttons">
|
||||
{#each buttons as button}
|
||||
<Button text on:click={button.method}>
|
||||
<span class="text">{button.name}</span>
|
||||
<Button text {...button} on:click={button.onClick}>
|
||||
<span class="text">{button.slot}</span>
|
||||
</Button>
|
||||
{/each}
|
||||
</slot>
|
||||
@@ -67,9 +68,9 @@
|
||||
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
|
||||
<slot name="buttons">
|
||||
{#each buttons as button}
|
||||
<button type="button" class="button is-text" on:click={button.method}>
|
||||
<span class="text">{button.name}</span>
|
||||
</button>
|
||||
<Button text {...button} on:click={button.onClick}>
|
||||
+ <span class="text">{button.slot}</span>
|
||||
</Button>
|
||||
{/each}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes, readOnly, tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
</script>
|
||||
|
||||
{#if $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && $readOnly && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`${$organization.name} usage has reached the ${tierToPlan($organization.billingPlan).name} plan limit`}>
|
||||
<svelte:fragment>
|
||||
Usage for the <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name}
|
||||
plan. Consider upgrading to increase your resource usage.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
href={`${base}/console/organization-${$organization.$id}/usage`}
|
||||
text
|
||||
fullWidthMobile>
|
||||
<span class="text">View usage</span>
|
||||
</Button>
|
||||
<Button
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'limit_reached_banner'
|
||||
});
|
||||
}}
|
||||
secondary
|
||||
fullWidthMobile>
|
||||
<span class="text">Upgrade plan</span>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
</script>
|
||||
|
||||
{#if $organization?.markedForDeletion && !$page.url.pathname.includes('/console/account')}
|
||||
{#if $organization?.markedForDeletion && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
<HeaderAlert title="Organization flagged for deletion">
|
||||
<svelte:fragment>
|
||||
All existing projects in the {$organization.name} organization have been paused. This organization
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
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';
|
||||
</script>
|
||||
|
||||
{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`Payment method required for ${$orgMissingPaymentMethod.name}`}>
|
||||
<svelte:fragment>
|
||||
Add a payment method to {$orgMissingPaymentMethod.name} to avoid service interruptions to
|
||||
your projects.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
secondary
|
||||
href={`/console/organization-${$orgMissingPaymentMethod.$id}/billing`}>
|
||||
Add payment method
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -2,14 +2,14 @@
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { actionRequiredInvoices } from '$lib/stores/billing';
|
||||
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`;
|
||||
</script>
|
||||
|
||||
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !$page.url.pathname.includes('/console/account')}
|
||||
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
<HeaderAlert title="Authorization required" type="error">
|
||||
Please authorize your upcoming payment for {$organization.name}. Your bank requires this
|
||||
security measure to proceed with payment.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes, paymentMissingMandate } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmSetup } from '$lib/stores/stripe';
|
||||
|
||||
async function verifyPaymentMethod() {
|
||||
const method = await sdk.forConsole.billing.setupPaymentMandate(
|
||||
$organization.$id,
|
||||
$paymentMissingMandate.$id
|
||||
);
|
||||
await confirmSetup(method.clientSecret, method.$id);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $paymentMissingMandate && $paymentMissingMandate.country === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
<HeaderAlert title="Authorization required" type="info">
|
||||
The payment method for {$organization.name} needs to be verified.
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button secondary on:click={verifyPaymentMethod}>Verify payment method</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -4,10 +4,10 @@
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { diffDays, toLocaleDate } from '$lib/helpers/date';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { failedInvoice } from '$lib/stores/billing';
|
||||
import { failedInvoice, hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
</script>
|
||||
|
||||
{#if $failedInvoice && !$page.url.pathname.includes('/console/account')}
|
||||
{#if $failedInvoice && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{@const daysPassed = diffDays(new Date($failedInvoice.dueAt), new Date())}
|
||||
<HeaderAlert title="Your projects are at risk">
|
||||
<svelte:fragment>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
|
||||
export let couponData: Partial<Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
export let fixedCoupon = false;
|
||||
</script>
|
||||
|
||||
{#if couponData?.credits}
|
||||
<span class="u-flex u-main-space-between">
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<p class="text">
|
||||
<span class="icon-tag u-color-text-success" aria-hidden="true" />
|
||||
{#if couponData.credits >= 100}
|
||||
{couponData?.code?.toUpperCase()}
|
||||
{:else}
|
||||
<span use:tooltip={{ content: couponData?.code?.toUpperCase() }}>
|
||||
Credits applied
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
{#if !fixedCoupon}
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
on:click={() =>
|
||||
(couponData = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
})}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if couponData.credits >= 100}
|
||||
<p class="inline-tag" use:tooltip={{ content: formatCurrency(couponData.credits) }}>
|
||||
Credits applied
|
||||
</p>
|
||||
{:else}
|
||||
<span class="u-color-text-success">-{formatCurrency(couponData.credits)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { Card } from '..';
|
||||
|
||||
export let service: string;
|
||||
export let eventSource: string;
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<slot>
|
||||
<div class="u-flex u-flex-vertical u-main-center u-cross-center u-gap-8">
|
||||
<h6 class="body-text-1 u-bold u-trim-1">Upgrade to add {service}</h6>
|
||||
<p class="text u-text-center">
|
||||
Upgrade to a {tierToPlan(BillingPlan.PRO).name} plan to add {service} to your organization
|
||||
</p>
|
||||
|
||||
<Button
|
||||
class="u-margin-block-start-16"
|
||||
secondary
|
||||
fullWidthMobile
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: eventSource
|
||||
});
|
||||
}}>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</slot>
|
||||
</Card>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputChoice, InputNumber } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { plansInfo, type Tier } from '$lib/stores/billing';
|
||||
import { CreditsApplied } from '.';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let collaborators: string[];
|
||||
export let couponData: Partial<Coupon>;
|
||||
export let billingBudget: number;
|
||||
export let fixedCoupon = false; // If true, the coupon cannot be removed
|
||||
|
||||
const today = new Date();
|
||||
const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
let budgetEnabled = false;
|
||||
|
||||
$: currentPlan = $plansInfo.get(billingPlan);
|
||||
$: extraSeatsCost = (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
|
||||
$: grossCost = currentPlan.price + extraSeatsCost;
|
||||
$: estimatedTotal =
|
||||
couponData?.status === 'active'
|
||||
? grossCost - couponData.credits >= 0
|
||||
? grossCost - couponData.credits
|
||||
: 0
|
||||
: grossCost;
|
||||
$: trialEndDate = new Date(
|
||||
billingPayDate.getTime() + currentPlan.trialDays * 24 * 60 * 60 * 1000
|
||||
);
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="card u-margin-block-start-32 u-flex u-flex-vertical u-gap-8"
|
||||
style:--p-card-padding="1.5rem"
|
||||
style:--p-card-border-radius="var(--border-radius-small)">
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">{currentPlan.name} plan</p>
|
||||
<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">
|
||||
{formatCurrency(extraSeatsCost)}
|
||||
</p>
|
||||
</span>
|
||||
{#if couponData?.status === 'active'}
|
||||
<CreditsApplied bind:couponData {fixedCoupon} />
|
||||
{/if}
|
||||
<div class="u-sep-block-start" />
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">Estimated total</p>
|
||||
<p class="text">
|
||||
{formatCurrency(estimatedTotal)}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<p class="text u-margin-block-start-16">
|
||||
Your payment method will be charged this amount plus usage fees every 30 days {!currentPlan.trialDays
|
||||
? `starting ${toLocaleDate(billingPayDate.toString())}`
|
||||
: ` after your trial period ends on ${toLocaleDate(trialEndDate.toString())}`}.
|
||||
</p>
|
||||
<FormList class="u-margin-block-start-24">
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id="budget"
|
||||
label="Enable budget cap"
|
||||
tooltip="If enabled, you will be notified when your spending reaches 75% of the set cap. Update cap alerts in your organization settings."
|
||||
fullWidth
|
||||
bind:value={budgetEnabled}>
|
||||
{#if budgetEnabled}
|
||||
<div class="u-margin-block-start-16">
|
||||
<InputNumber
|
||||
id="budget"
|
||||
label="Budget cap (USD)"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
bind:value={billingBudget} />
|
||||
</div>
|
||||
{/if}
|
||||
</InputChoice>
|
||||
</FormList>
|
||||
</section>
|
||||
@@ -1,2 +1,8 @@
|
||||
export { default as PaymentBoxes } from './paymentBoxes.svelte';
|
||||
export { default as CouponInput } from './couponInput.svelte';
|
||||
export { default as SelectPaymentMethod } from './selectPaymentMethod.svelte';
|
||||
export { default as UsageRates } from './usageRates.svelte';
|
||||
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';
|
||||
|
||||
@@ -46,6 +46,11 @@
|
||||
$: if (element) {
|
||||
observer.observe(element, { childList: true });
|
||||
}
|
||||
|
||||
//Set setAsDefault as false when group changes
|
||||
$: if (group || group === null) {
|
||||
setAsDefault = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<RadioBoxes
|
||||
|
||||
+11
-3
@@ -6,6 +6,7 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let show = false;
|
||||
|
||||
@@ -20,7 +21,7 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const card = await submitStripeCard(name);
|
||||
const card = await submitStripeCard(name, $page?.params?.organization ?? null);
|
||||
invalidate(Dependencies.PAYMENT_METHODS);
|
||||
dispatch('submit', card);
|
||||
show = false;
|
||||
@@ -67,8 +68,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FakeModal bind:show title="Add payment method" bind:error onSubmit={handleSubmit}>
|
||||
<FakeModal
|
||||
bind:show
|
||||
title="Add payment method"
|
||||
bind:error
|
||||
onSubmit={handleSubmit}
|
||||
headerDivider={false}>
|
||||
<FormList gap={16}>
|
||||
<slot />
|
||||
<InputText
|
||||
id="name"
|
||||
label="Cardholder name"
|
||||
@@ -85,10 +92,11 @@
|
||||
<!-- Stripe will create form elements here -->
|
||||
</div>
|
||||
</div>
|
||||
<slot name="end"></slot>
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit disabled={!name}>Save</Button>
|
||||
<Button submit disabled={!name}>Add</Button>
|
||||
</svelte:fragment>
|
||||
</FakeModal>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { Box, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
|
||||
let selectedTab: 'tier-0' | 'tier-1' = 'tier-0';
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
</script>
|
||||
|
||||
<Box>
|
||||
<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>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
{#if selectedTab === 'tier-0'}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
<li>
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions} Functions
|
||||
per project
|
||||
</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
</ul>
|
||||
{:else if selectedTab === 'tier-1'}
|
||||
<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>
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
</Box>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { Alert } from '$lib/components';
|
||||
import { calculateExcess, plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { abbreviateNumber } from '$lib/helpers/numbers';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { onMount } from 'svelte';
|
||||
import type { OrganizationUsage } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
export let tier: Tier;
|
||||
|
||||
const plan = $plansInfo?.get(tier);
|
||||
let excess: {
|
||||
bandwidth?: number;
|
||||
storage?: number;
|
||||
users?: number;
|
||||
executions?: number;
|
||||
members?: number;
|
||||
} = null;
|
||||
let usage: OrganizationUsage = null;
|
||||
let showExcess = false;
|
||||
|
||||
onMount(async () => {
|
||||
usage = await sdk.forConsole.billing.listUsage(
|
||||
$organization.$id,
|
||||
$organization.billingCurrentInvoiceDate,
|
||||
new Date().toISOString()
|
||||
);
|
||||
excess = calculateExcess(usage, plan, $organization);
|
||||
showExcess = Object.values(excess).some((value) => value > 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if showExcess}
|
||||
<Alert type="error" {...$$restProps}>
|
||||
<svelte:fragment slot="title">
|
||||
Your {tierToPlan($organization.billingPlan).name} plan subscription will end on {toLocaleDate(
|
||||
$organization.billingNextInvoiceDate
|
||||
)}
|
||||
</svelte:fragment>
|
||||
Following payment of your final invoice, your organization will switch to the {tierToPlan(
|
||||
BillingPlan.FREE
|
||||
).name} plan. {#if excess?.members > 0}All team members except the owner will be removed on
|
||||
that date.{/if} Service disruptions may occur unless resource usage is reduced.
|
||||
<!-- Any executions, bandwidth, or messaging usage will be reset at that time. -->
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
text
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/free#reaching-resource-limits">
|
||||
Learn more
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Alert>
|
||||
|
||||
<TableScroll dense noMargin class="u-margin-block-start-16">
|
||||
<TableHeader>
|
||||
<TableCellHead>Resource</TableCellHead>
|
||||
<TableCellHead>Free limit</TableCellHead>
|
||||
<TableCellHead>Excess usage</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#if excess?.members}
|
||||
<TableRow>
|
||||
<TableCellText title="members">Organization members</TableCellText>
|
||||
<TableCellText title="limit">{plan.members} members</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger u-flex u-cross-center u-gap-4">
|
||||
<span class="icon-arrow-up" />
|
||||
{excess?.members} members
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{#if excess?.storage}
|
||||
<TableRow>
|
||||
<TableCellText title="storage">Storage</TableCellText>
|
||||
<TableCellText title="limit">{plan.storage} GB</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
{humanFileSize(excess?.storage)}
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{#if excess?.executions}
|
||||
<TableRow>
|
||||
<TableCellText title="executions">Function executions</TableCellText>
|
||||
<TableCellText title="limit">
|
||||
{abbreviateNumber(plan.executions)} executions
|
||||
</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
<span
|
||||
title={excess?.executions
|
||||
? excess.executions.toString()
|
||||
: 'executions'}>
|
||||
{formatNum(excess?.executions)} executions
|
||||
</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{#if excess?.users}
|
||||
<TableRow>
|
||||
<TableCellText title="users">Users</TableCellText>
|
||||
<TableCellText title="limit">
|
||||
{abbreviateNumber(plan.users)} users
|
||||
</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
<span title={excess?.users ? excess.users.toString() : 'users'}>
|
||||
{formatNum(excess?.users)} users
|
||||
</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
{/if}
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { Button, InputChoice, InputSelectSearch, InputText } from '$lib/elements/forms';
|
||||
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
import { Alert, Card, CreditCardBrandImage } from '..';
|
||||
import PaymentModal from './paymentModal.svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
|
||||
export let methods: PaymentList;
|
||||
export let value: string;
|
||||
export let taxId = '';
|
||||
|
||||
let showTaxId = false;
|
||||
let showPaymentModal = false;
|
||||
|
||||
async function cardSaved(event: CustomEvent<PaymentMethodData>) {
|
||||
value = event.detail.$id;
|
||||
methods = await sdk.forConsole.billing.listPaymentMethods();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (methods?.total && methods.paymentMethods.some((method) => !!method?.last4)) {
|
||||
value = methods.paymentMethods[0].$id;
|
||||
}
|
||||
});
|
||||
|
||||
$: filteredMethods = methods?.paymentMethods?.filter((method) => !!method?.last4);
|
||||
|
||||
$: selectedPaymentMethod = methods?.paymentMethods?.find((method) => method.$id === value);
|
||||
</script>
|
||||
|
||||
{#if filteredMethods?.length}
|
||||
{#if selectedPaymentMethod?.country === 'in'}
|
||||
<Alert type="warning">
|
||||
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
|
||||
To comply with RBI regulations in India, Appwrite will ask for verification to charge up
|
||||
to $150 USD on your payment method. We will never charge more than the cost of your plan
|
||||
and the resources you use, or your budget cap limit. For higher usage limits, please contact
|
||||
us.
|
||||
</Alert>
|
||||
{/if}
|
||||
<InputSelectSearch
|
||||
id="method"
|
||||
required
|
||||
label="Payment method"
|
||||
placeholder="Select payment method"
|
||||
bind:value
|
||||
options={filteredMethods.map((method) => {
|
||||
return {
|
||||
value: method.$id,
|
||||
label: `${capitalize(method.brand)} ending in ${method.last4}`,
|
||||
data: [method.brand]
|
||||
};
|
||||
})}
|
||||
interactiveOutput
|
||||
let:option={o}>
|
||||
<svelte:fragment slot="output" let:option={o}>
|
||||
<output class="input-text u-cursor-pointer">
|
||||
<span class="u-flex u-gap-16 u-flex-vertical">
|
||||
<span class="u-flex u-gap-16">
|
||||
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
|
||||
<span>{o.label}</span>
|
||||
<CreditCardBrandImage brand={o.data?.toString()} />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</output>
|
||||
</svelte:fragment>
|
||||
|
||||
<span class="u-flex u-gap-16 u-flex-vertical">
|
||||
<span class="u-flex u-gap-16">
|
||||
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
|
||||
<span>{o.label}</span>
|
||||
<CreditCardBrandImage brand={o.data?.toString()} />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<svelte:fragment slot="listEnd">
|
||||
<Button text on:click={() => (showPaymentModal = true)}>
|
||||
<span class="icon-plus"></span>
|
||||
<span class="text">Add new payment method</span>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</InputSelectSearch>
|
||||
{:else}
|
||||
<Card
|
||||
isDashed
|
||||
style="--p-card-padding:0.75rem; --p-card-bg-color: transparent; --p-card-border-radius: 0.5rem"
|
||||
isTile>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p class="u-flex u-gap-8 u-cross-center">
|
||||
<span
|
||||
class="icon-exclamation u-color-text-warning"
|
||||
style:font-size="var(--icon-size-medium)"></span>
|
||||
<span class="text">A payment method is required</span>
|
||||
</p>
|
||||
<Button secondary on:click={() => (showPaymentModal = true)}>
|
||||
<span class="icon-plus"></span> <span class="text">Add</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if showPaymentModal && isCloud && hasStripePublicKey}
|
||||
<PaymentModal bind:show={showPaymentModal} on:submit={cardSaved}>
|
||||
<svelte:fragment slot="end">
|
||||
<InputChoice
|
||||
type="checkbox"
|
||||
id="taxIdCheck"
|
||||
label="I'm purchasing as a business"
|
||||
fullWidth
|
||||
bind:value={showTaxId}>
|
||||
{#if showTaxId}
|
||||
<div class="u-margin-block-start-8">
|
||||
<InputText
|
||||
id="taxId"
|
||||
label="Tax ID"
|
||||
placeholder="Tax ID"
|
||||
bind:value={taxId} />
|
||||
</div>
|
||||
{/if}
|
||||
</InputChoice>
|
||||
</svelte:fragment>
|
||||
</PaymentModal>
|
||||
{/if}
|
||||
+10
-12
@@ -10,18 +10,17 @@
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { createOrganization } from './store';
|
||||
import { plansInfo, type Tier } from '$lib/stores/billing';
|
||||
import { organization, type Organization } from '$lib/stores/organization';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { abbreviateNumber, formatCurrency } from '$lib/helpers/numbers';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
export let tier: Tier;
|
||||
export let org: Organization;
|
||||
|
||||
$: plan = $plansInfo?.get(tier);
|
||||
$: plan = $plansInfo?.get(org.billingPlan);
|
||||
|
||||
$: nextDate = $createOrganization?.name
|
||||
$: nextDate = org?.name
|
||||
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
|
||||
: $organization?.billingNextInvoiceDate;
|
||||
|
||||
@@ -50,20 +49,19 @@
|
||||
}
|
||||
];
|
||||
|
||||
$: isFree = tier === BillingPlan.STARTER;
|
||||
$: isFree = org.billingPlan === BillingPlan.FREE;
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="big" headerDivider={false} title="Usage rates">
|
||||
{#if isFree}
|
||||
Usage on the Starter plan is limited for the following resources. Next billing period: {toLocaleDate(
|
||||
nextDate
|
||||
)}.
|
||||
{:else if tier === BillingPlan.PRO}
|
||||
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
|
||||
Next billing period: {toLocaleDate(nextDate)}.
|
||||
{:else if org.billingPlan === BillingPlan.PRO}
|
||||
<p>
|
||||
Usage on the Pro plan will be charged at the end of each billing period at the following
|
||||
rates. Next billing period: {toLocaleDate(nextDate)}.
|
||||
</p>
|
||||
{:else if tier === BillingPlan.SCALE}
|
||||
{:else if org.billingPlan === BillingPlan.SCALE}
|
||||
<p>
|
||||
Usage on the Scale plan will be charged at the end of each billing period at the
|
||||
following rates. Next billing period: {toLocaleDate(nextDate)}.
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList, InputText } from '$lib/elements/forms';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let show = false;
|
||||
let error: string = null;
|
||||
let coupon: string = '';
|
||||
export let couponData: Partial<Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
|
||||
async function addCoupon() {
|
||||
try {
|
||||
const response = await sdk.forConsole.billing.getCoupon(coupon);
|
||||
couponData = response;
|
||||
dispatch('validation', couponData);
|
||||
coupon = null;
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Credits applied successfully'
|
||||
});
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (coupon) {
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
bind:show
|
||||
title="Add credits"
|
||||
headerDivider={false}
|
||||
onSubmit={addCoupon}
|
||||
size="big"
|
||||
bind:error>
|
||||
Credits will be applied automatically to your next invoice.
|
||||
|
||||
<FormList>
|
||||
<InputText placeholder="Promo code" id="code" label="Add promo code" bind:value={coupon} />
|
||||
</FormList>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Add</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -5,6 +5,8 @@
|
||||
isTile?: boolean;
|
||||
isDashed?: boolean;
|
||||
danger?: boolean;
|
||||
style?: string;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
@@ -26,6 +28,7 @@
|
||||
export let href: string = null;
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
export let style: string = '';
|
||||
|
||||
function getElement() {
|
||||
switch (true) {
|
||||
@@ -46,6 +49,8 @@
|
||||
class:is-border-dashed={isDashed}
|
||||
class:is-danger={danger}
|
||||
class:is-allowed-focus={href}
|
||||
{...$$restProps}
|
||||
{style}
|
||||
on:click
|
||||
on:keyup={clickOnEnter}
|
||||
role={href || isButton ? 'button' : 'generic'}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<ul
|
||||
class="grid-box common-section u-margin-block-start-32"
|
||||
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
|
||||
style={`--grid-gap:1.5rem; --grid-item-size-small-screens: 18rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
|
||||
data-private>
|
||||
<slot />
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
export let hideFooter = false;
|
||||
</script>
|
||||
|
||||
<Card {danger}>
|
||||
<Card {danger} class={danger ? 'u-padding-inline-start-32' : ''}>
|
||||
<div class="common-section grid-1-2" class:hideOverflow>
|
||||
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
|
||||
<slot />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
|
||||
export let service: string;
|
||||
</script>
|
||||
@@ -9,8 +8,6 @@
|
||||
<article class="card u-grid u-cross-center u-width-full-line">
|
||||
<div class="u-flex u-flex-vertical u-gap-24 u-main-center u-cross-center">
|
||||
<p class="text u-text-center">Upgrade your plan to add more {service}</p>
|
||||
<Button secondary on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
|
||||
Change plan
|
||||
</Button>
|
||||
<Button secondary href={$upgradeURL}>Change plan</Button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
export let href: string = null;
|
||||
export let external: boolean = false;
|
||||
</script>
|
||||
|
||||
<li class="clickable-list-item">
|
||||
<a {href} class="clickable-list-button" on:click>
|
||||
{#if $$slots.default}
|
||||
<h5 class="clickable-list-title u-trim-1">
|
||||
<slot />
|
||||
</h5>
|
||||
{/if}
|
||||
{#if $$slots.desc}
|
||||
<div class="clickable-list-desc">
|
||||
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{#if href}
|
||||
<a
|
||||
{href}
|
||||
class="clickable-list-button"
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
on:click>
|
||||
{#if $$slots.default}
|
||||
<h5 class="clickable-list-title u-trim-1">
|
||||
<slot />
|
||||
</h5>
|
||||
{/if}
|
||||
{#if $$slots.desc}
|
||||
<div class="clickable-list-desc">
|
||||
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button class="clickable-list-button u-width-full-line" on:click>
|
||||
{#if $$slots.default}
|
||||
<h5 class="clickable-list-title u-trim-1">
|
||||
<slot />
|
||||
</h5>
|
||||
{/if}
|
||||
{#if $$slots.desc}
|
||||
<div class="clickable-list-desc">
|
||||
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<ul class="collapsible">
|
||||
<script lang="ts">
|
||||
export let fullWidth = false;
|
||||
</script>
|
||||
|
||||
<ul class="collapsible" class:u-width-full-line={fullWidth}>
|
||||
<slot />
|
||||
</ul>
|
||||
|
||||
@@ -3,33 +3,74 @@
|
||||
|
||||
export let withIndentation = false;
|
||||
export let open = false;
|
||||
export let disabled = false;
|
||||
export let noContent = false;
|
||||
export let isInfo = false;
|
||||
export let gap = 16;
|
||||
</script>
|
||||
|
||||
<li class="collapsible-item">
|
||||
<details class="collapsible-wrapper" {open}>
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class="collapsible-button u-position-relative"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
<span class="text"><slot name="title" /></span>
|
||||
{#if $$slots.subtitle}
|
||||
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
|
||||
{/if}
|
||||
<li class="collapsible-item" class:is-info={isInfo}>
|
||||
{#if noContent}
|
||||
<div class="collapsible-wrapper">
|
||||
<div class={`collapsible-button u-gap-${gap}`}>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="icon">
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
</div>
|
||||
</summary>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
class:u-margin-block-start-8={withIndentation}
|
||||
class:u-padding-inline-32={withIndentation}>
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<details class="collapsible-wrapper" class:is-disabled={disabled} bind:open>
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class={`collapsible-button u-position-relative u-gap-${gap}`}
|
||||
on:keyup={clickOnEnter}
|
||||
on:click
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
<span class="text" class:u-color-text-disabled={disabled}>
|
||||
<slot name="title" /></span>
|
||||
{#if $$slots.subtitle}
|
||||
<span
|
||||
class="collapsible-button-optional"
|
||||
class:u-color-text-disabled={disabled}><slot name="subtitle" /></span>
|
||||
{/if}
|
||||
</div>
|
||||
<slot name="end">
|
||||
<div class="icon">
|
||||
<span
|
||||
class="icon-cheveron-down u-font-size-20"
|
||||
class:u-color-text-disabled={disabled}
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
</slot>
|
||||
</summary>
|
||||
<div
|
||||
class="collapsible-content"
|
||||
class:u-margin-block-start-8={withIndentation}
|
||||
class:u-padding-inline-32={withIndentation}>
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style lang="scss">
|
||||
// TODO: remove once pink is updated
|
||||
.collapsible-item {
|
||||
.collapsible-wrapper.is-disabled {
|
||||
&[open] .icon-cheveron-down {
|
||||
rotate: unset;
|
||||
}
|
||||
|
||||
cursor: not-allowed;
|
||||
* {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.collapsible {
|
||||
&-content {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Label } from '$lib/elements/forms';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import { Copy, Drop } from '.';
|
||||
|
||||
export let value: string;
|
||||
export let label: string = null;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = false;
|
||||
export let labelTooltip: string = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let appendTo: Parameters<typeof tooltip>['1']['appendTo'] = undefined;
|
||||
|
||||
let content = 'Click to copy';
|
||||
|
||||
const handleCopy = async () => {
|
||||
const success = await copy(value);
|
||||
|
||||
if (success) {
|
||||
content = 'Copied';
|
||||
} else {
|
||||
addNotification({
|
||||
message: 'Unable to copy to clipboard',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<label class:u-hide={!showLabel} class="label" for={label}>{label}</label>
|
||||
{#if label}
|
||||
<Label tooltip={labelTooltip} {optionalText} hide={!showLabel} for={label}>
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style:font-size="var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1">
|
||||
<input {value} id={label} type="text" class="input-text" readonly />
|
||||
<input id={label} type="text" {value} readonly />
|
||||
<div class="options-list">
|
||||
<button
|
||||
type="button"
|
||||
class="options-list-button"
|
||||
aria-label="Click to copy."
|
||||
on:click={handleCopy}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
hideOnClick: false
|
||||
}}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
{#key appendTo}
|
||||
<Copy {value} {appendTo}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { CreditCard } from '@appwrite.io/console';
|
||||
|
||||
export let brand: string;
|
||||
export let width = 23;
|
||||
export let height = 16;
|
||||
|
||||
function getCreditCardImage(brand: string, width = 46, height = 32) {
|
||||
if (!brand) return '';
|
||||
if (!isValueOfStringEnum(CreditCard, brand)) return '';
|
||||
return sdk.forConsole.avatars.getCreditCard(brand, width, height).toString();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,4 +33,9 @@
|
||||
<svelte:fragment slot="title">This payment method has expired</svelte:fragment>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if paymentMethod?.lastError}
|
||||
<Alert type="error" class="u-margin-block-start-16 u-width-full-line">
|
||||
{paymentMethod.lastError}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
export let childStart = false;
|
||||
export let noStyle = false;
|
||||
export let fullWidth = false;
|
||||
export let wrapperFullWidth = false;
|
||||
export let fixed = false;
|
||||
export let display = 'block';
|
||||
export let arrowSize = 10;
|
||||
export let isPopover = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
blur: undefined;
|
||||
@@ -31,13 +35,14 @@
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: arrow
|
||||
element: arrow,
|
||||
padding: arrowSize * 1.75
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, noArrow ? 0 : 6]
|
||||
offset: [noArrow ? 0 : -arrowSize, noArrow ? 0 : arrowSize / 1.5]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -100,16 +105,27 @@
|
||||
|
||||
<svelte:window on:click={onBlur} on:keydown={onKeyDown} />
|
||||
|
||||
<div class:drop-wrapper={!noStyle} class:u-cross-child-start={childStart} bind:this={element}>
|
||||
<div
|
||||
class="u-min-width-0"
|
||||
class:drop-wrapper={!noStyle}
|
||||
class:u-cross-child-start={childStart}
|
||||
class:u-width-full-line={wrapperFullWidth}
|
||||
bind:this={element}
|
||||
style:display>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="drop-tooltip"
|
||||
class:u-width-full-line={fullWidth}
|
||||
bind:this={tooltip}
|
||||
class:u-width-full-line={fullWidth}
|
||||
style:--arrow-size={`${arrowSize}px`}
|
||||
style:z-index="10">
|
||||
<div class="drop-arrow" class:u-hide={!show || (show && noArrow)} bind:this={arrow} />
|
||||
<div
|
||||
class="drop-arrow"
|
||||
class:is-popover={isPopover}
|
||||
class:u-hide={!show || (show && noArrow)}
|
||||
bind:this={arrow} />
|
||||
{#if show}
|
||||
<slot name="list" />
|
||||
{/if}
|
||||
@@ -117,36 +133,50 @@
|
||||
|
||||
<!-- svelte-ignore css-unused-selector -->
|
||||
<style global lang="scss">
|
||||
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='left'] > .drop-arrow {
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='right'] > .drop-arrow {
|
||||
left: -4px;
|
||||
.drop-arrow.is-popover {
|
||||
--drop-arrow-pop-over-bg-color: var(--color-neutral-90);
|
||||
body.theme-light & {
|
||||
--drop-arrow-pop-over-bg-color: var(--color-neutral-0);
|
||||
}
|
||||
}
|
||||
.drop-arrow,
|
||||
.drop-arrow::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: -1;
|
||||
width: var(--arrow-size);
|
||||
height: var(--arrow-size);
|
||||
z-index: 1;
|
||||
|
||||
--drop-arrow-border: 1px solid hsl(var(--color-neutral-85));
|
||||
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-105)));
|
||||
body.theme-light & {
|
||||
--drop-arrow-border: 1px solid hsl(var(--color-neutral-10));
|
||||
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-0)));
|
||||
}
|
||||
}
|
||||
|
||||
.drop-arrow::before {
|
||||
content: '';
|
||||
transform: rotate(45deg);
|
||||
background: hsl(var(--color-neutral-85));
|
||||
background: var(--drop-arrow-bg-color);
|
||||
}
|
||||
|
||||
body.theme-light & {
|
||||
background: hsl(var(--color-neutral-10));
|
||||
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
|
||||
bottom: calc(var(--arrow-size) / -2);
|
||||
|
||||
&::before {
|
||||
border-bottom: var(--drop-arrow-border);
|
||||
border-right: var(--drop-arrow-border);
|
||||
border-bottom-right-radius: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
|
||||
top: calc(var(--arrow-size) / -2);
|
||||
|
||||
&::before {
|
||||
border-top: var(--drop-arrow-border);
|
||||
border-left: var(--drop-arrow-border);
|
||||
border-top-left-radius: 25%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,10 +11,21 @@
|
||||
export let noStyle = false;
|
||||
export let width: string = null;
|
||||
export let fullWidth = false;
|
||||
export let wrapperFullWidth = false;
|
||||
export let position: 'relative' | 'static' = 'relative';
|
||||
export let noMaxWidthList = false;
|
||||
</script>
|
||||
|
||||
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed} on:blur>
|
||||
<Drop
|
||||
bind:show
|
||||
{placement}
|
||||
{childStart}
|
||||
{noArrow}
|
||||
{noStyle}
|
||||
{fullWidth}
|
||||
{wrapperFullWidth}
|
||||
{fixed}
|
||||
on:blur>
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
@@ -25,6 +36,7 @@
|
||||
}`}>
|
||||
{#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">
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{#if single}
|
||||
<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-width-full-line u-overflow-hidden">
|
||||
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-width-full-line u-overflow-hidden u-padding-block-8">
|
||||
{#if !noMedia}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { EmptySearch } from '.';
|
||||
import { queries } from './filters';
|
||||
|
||||
export let resource;
|
||||
</script>
|
||||
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry, we couldn't find any {resource}.</b>
|
||||
<p>There are no {resource} that match your filters.</p>
|
||||
</div>
|
||||
<div class="u-flex common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
@@ -249,7 +249,7 @@
|
||||
<slot />
|
||||
<div>
|
||||
<p class="u-text">Choose a service</p>
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8">
|
||||
<div class="u-flex u-gap-8 u-margin-block-start-8 u-flex-wrap">
|
||||
{#each available.services as service}
|
||||
<Pill
|
||||
disabled={showInput}
|
||||
@@ -332,7 +332,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2" bind:this={copyParent}>
|
||||
<!--
|
||||
<!--
|
||||
This object syntax avoids TS erroring because 'type' isn't a valid HTMLDivElement attribute
|
||||
(we need to set it to 'text' to add styling)
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
<script lang="ts">
|
||||
import { Id, ModalWrapper, Trim } from '.';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ID, Query, Permission, Role } from '@appwrite.io/console';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { calculateSize } from '$lib/helpers/sizeConvertion';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableRowButton,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableCellText,
|
||||
TableCellHead
|
||||
} from '$lib/elements/table';
|
||||
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import FormList from '$lib/elements/forms/formList.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import Heading from './heading.svelte';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import Empty from './empty.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let show: boolean;
|
||||
export let mimeTypeQuery: string = 'image/';
|
||||
export let selectedBucket: string = null;
|
||||
export let selectedFile: string = null;
|
||||
export let onSelect: (e: Models.File) => void;
|
||||
|
||||
let search = writable('');
|
||||
let searchEnabled = false;
|
||||
let fileSelector: HTMLInputElement;
|
||||
let uploading = false;
|
||||
let view: 'grid' | 'list' = 'list';
|
||||
|
||||
onMount(() => {
|
||||
selectedBucket = currentBucket?.$id;
|
||||
});
|
||||
|
||||
function submitForm() {
|
||||
onSelect(currentFile);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function getPreview(bucketId: string, fileId: string, size: number = 64) {
|
||||
return (
|
||||
sdk.forProject.storage.getFilePreview(bucketId, fileId, size, size).toString() +
|
||||
'&mode=admin'
|
||||
);
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
try {
|
||||
uploading = true;
|
||||
const file = await sdk.forProject.storage.createFile(
|
||||
selectedBucket,
|
||||
ID.unique(),
|
||||
fileSelector.files[0],
|
||||
[Permission.read(Role.any())]
|
||||
);
|
||||
search.set($search === null ? '' : null);
|
||||
selectFile(file);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectBucket(bucket: Models.Bucket | null) {
|
||||
currentBucket = bucket;
|
||||
selectedBucket = bucket?.$id ?? null;
|
||||
resetFile();
|
||||
}
|
||||
|
||||
function selectFile(file: Models.File) {
|
||||
currentFile = file;
|
||||
selectedBucket = currentFile.bucketId;
|
||||
selectedFile = currentFile.$id;
|
||||
}
|
||||
|
||||
function resetFile() {
|
||||
selectedFile = null;
|
||||
}
|
||||
|
||||
function resetBucket() {
|
||||
selectedBucket = null;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
show = false;
|
||||
resetFile();
|
||||
resetBucket();
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (!document.hidden) {
|
||||
buckets = loadBuckets();
|
||||
}
|
||||
}
|
||||
|
||||
let currentBucket: Models.Bucket = null;
|
||||
let currentFile: Models.File = null;
|
||||
let buckets: Promise<Models.BucketList> = loadBuckets();
|
||||
async function loadBuckets() {
|
||||
const response = await sdk.forProject.storage.listBuckets();
|
||||
const bucket = response.buckets[0] ?? null;
|
||||
if (bucket) {
|
||||
currentBucket = bucket;
|
||||
selectedBucket = bucket.$id;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
$: files =
|
||||
currentBucket &&
|
||||
sdk.forProject.storage
|
||||
.listFiles(
|
||||
currentBucket.$id,
|
||||
[Query.startsWith('mimeType', mimeTypeQuery), Query.orderDesc('$createdAt')],
|
||||
$search || undefined
|
||||
)
|
||||
.then((response) => {
|
||||
if ($search === '') {
|
||||
searchEnabled = response.total > 0;
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
$: if ($search) {
|
||||
resetFile();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:document on:visibilitychange={handleVisibilityChange} />
|
||||
|
||||
<ModalWrapper bind:show size="huge">
|
||||
<Form isModal onSubmit={submitForm} class="u-stretch">
|
||||
<header class="modal-header u-margin-block-end-0">
|
||||
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
|
||||
<h4 class="modal-title heading-level-5">Select file</h4>
|
||||
<button
|
||||
type="button"
|
||||
on:click={closeModal}
|
||||
class="button is-text is-small is-only-icon"
|
||||
aria-label="Close modal">
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="modal-content u-stretch u-flex-vertical u-padding-0 u-margin-block-0 u-overflow-visible">
|
||||
<div class="u-flex u-min-height-0 u-stretch">
|
||||
<aside
|
||||
class="drop-section u-width-200 u-padding-16
|
||||
u-flex-vertical u-gap-8
|
||||
u-flex-shrink-0 u-margin-inline-0 u-overflow-y-auto is-not-mobile">
|
||||
<h6
|
||||
class="eyebrow-heading-3"
|
||||
style:--heading-text-color="var(--color-neutral-50)">
|
||||
Buckets
|
||||
</h6>
|
||||
<ul class="drop-list">
|
||||
{#await buckets}
|
||||
<div class="u-flex u-main-center">
|
||||
<div class="loader" />
|
||||
</div>
|
||||
{:then response}
|
||||
{#each response.buckets as bucket}
|
||||
{@const isSelected = bucket.$id === selectedBucket}
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
type="button"
|
||||
class="drop-button"
|
||||
class:is-selected={isSelected}
|
||||
on:click={() => selectBucket(bucket)}>
|
||||
<span>{bucket.name}</span>
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="drop-list-item">
|
||||
<span class="drop-button">No buckets found</span>
|
||||
</li>
|
||||
{/each}
|
||||
{/await}
|
||||
</ul>
|
||||
</aside>
|
||||
<article
|
||||
style:padding-inline="calc(var(--p-modal-padding))"
|
||||
class="modal-content-main u-flex-vertical u-gap-24 u-sep-inline-start u-flex-basis-1000 u-padding-block-24 u-overflow-y-auto">
|
||||
<div class="is-only-mobile">
|
||||
{#await buckets}
|
||||
loading
|
||||
{:then response}
|
||||
{#if currentBucket?.$id}
|
||||
<FormList>
|
||||
<InputSelect
|
||||
wrapperTag="div"
|
||||
label="Buckets"
|
||||
options={response.buckets.map((n) => ({
|
||||
value: n.$id,
|
||||
label: n.name
|
||||
}))}
|
||||
bind:value={currentBucket.$id}
|
||||
id="buckets" />
|
||||
</FormList>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
{#await buckets}
|
||||
<div class="u-flex-vertical u-stretch u-position-relative u-main-center">
|
||||
<div
|
||||
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
|
||||
style="inset-inline-start: 0;">
|
||||
<div class="loader" />
|
||||
<p class="text">Loading files...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:then response}
|
||||
{#if response?.total}
|
||||
{#if currentBucket}
|
||||
<header class="u-flex-vertical u-gap-32">
|
||||
<div class="u-flex u-gap-16">
|
||||
<h5 class="heading-level-6 u-trim u-min-width-0">
|
||||
{currentBucket?.name}
|
||||
</h5>
|
||||
<Id value={currentBucket?.$id} event="bucket">
|
||||
{currentBucket?.$id}
|
||||
</Id>
|
||||
</div>
|
||||
<div
|
||||
class="u-flex u-main-space-between u-gap-16 u-flex-vertical-mobile">
|
||||
<InputSearch
|
||||
placeholder="Search files"
|
||||
bind:value={$search}
|
||||
disabled={!searchEnabled}
|
||||
style="min-inline-size: 17.5rem; block-size: 100%" />
|
||||
<div class="u-flex u-gap-16">
|
||||
<div class="toggle-button">
|
||||
<ul class="toggle-button-list">
|
||||
<li class="toggle-button-item">
|
||||
<button
|
||||
on:click={() => (view = 'list')}
|
||||
disabled={!searchEnabled}
|
||||
type="button"
|
||||
class="toggle-button-element"
|
||||
class:is-selected={view === 'list'}
|
||||
aria-label="List View">
|
||||
<span
|
||||
class="icon-view-list"
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
<li class="toggle-button-item">
|
||||
<button
|
||||
on:click={() => (view = 'grid')}
|
||||
disabled={!searchEnabled}
|
||||
type="button"
|
||||
class="toggle-button-element"
|
||||
class:is-selected={view === 'grid'}
|
||||
aria-label="Grid View">
|
||||
<span
|
||||
class="icon-view-grid"
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
class="is-full-width-in-stack-mobile u-height-100-percent"
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="u-hide"
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"
|
||||
></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if files}
|
||||
{#await files}
|
||||
<div
|
||||
class="u-flex-vertical u-stretch u-position-relative u-main-center">
|
||||
<div
|
||||
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
|
||||
style="inset-inline-start: 0;">
|
||||
<div class="loader" />
|
||||
<p class="text">Loading files...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:then response}
|
||||
<div class="u-flex-vertical u-stretch">
|
||||
{#if response?.files?.length}
|
||||
{#if view === 'grid'}
|
||||
<ul
|
||||
class="grid-box"
|
||||
style="--grid-gap:40px; --grid-item-size:120px; --grid-item-size-small-screens:100px;">
|
||||
{#each response?.files as file}
|
||||
<li>
|
||||
<div
|
||||
class="u-flex-vertical u-gap-8">
|
||||
<div
|
||||
role="button"
|
||||
style:background-size="cover"
|
||||
style:background-image={`url(${getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id,
|
||||
360
|
||||
)})`}
|
||||
on:click={() =>
|
||||
selectFile(file)}
|
||||
on:keyup={clickOnEnter}
|
||||
tabindex="0"
|
||||
style:aspect-ratio="1/1"
|
||||
style:display="flex"
|
||||
style:align-items="flex-end"
|
||||
style:flex-direction="row-reverse"
|
||||
style:box-shadow="none"
|
||||
class="card u-height-100-percent u-gap-16"
|
||||
style="--card-padding:0.5rem;--card-padding-mobile:0.5rem; --card-border-radius:var(--border-radius-medium);">
|
||||
<input
|
||||
class="u-position-absolute is-small u-margin-block-start-2"
|
||||
type="radio"
|
||||
name="file"
|
||||
value={file.$id}
|
||||
style:pointer-events="none"
|
||||
checked={selectedFile ===
|
||||
file.$id} />
|
||||
</div>
|
||||
<span class="u-text-center"
|
||||
><Trim alternativeTrim
|
||||
>{file.name}</Trim
|
||||
></span>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if view === 'list'}
|
||||
<Table noMargin noStyles transparent dense>
|
||||
<TableHeader>
|
||||
<TableCellHead
|
||||
><span
|
||||
class="u-margin-inline-start-8"
|
||||
>Filename</span
|
||||
></TableCellHead>
|
||||
<TableCellHead width={140} onlyDesktop>
|
||||
ID
|
||||
</TableCellHead>
|
||||
<TableCellHead width={100} onlyDesktop>
|
||||
Type
|
||||
</TableCellHead>
|
||||
<TableCellHead width={100} onlyDesktop>
|
||||
Size
|
||||
</TableCellHead>
|
||||
<TableCellHead width={120} onlyDesktop>
|
||||
Created
|
||||
</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each response?.files as file}
|
||||
<TableRowButton
|
||||
on:click={() =>
|
||||
selectFile(file)}>
|
||||
<TableCell title="Filename">
|
||||
<div
|
||||
class="u-inline-flex u-cross-center u-gap-12">
|
||||
<input
|
||||
type="radio"
|
||||
class="is-small u-margin-inline-start-8"
|
||||
name="file"
|
||||
value={file.$id}
|
||||
style:pointer-events="none"
|
||||
checked={selectedFile ===
|
||||
file.$id} />
|
||||
<span class="image">
|
||||
<img
|
||||
class="avatar"
|
||||
style:border-radius="var(--border-radius-xsmall)"
|
||||
width="28"
|
||||
height="28"
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id
|
||||
)}
|
||||
alt={file.name} />
|
||||
</span>
|
||||
<Trim alternativeTrim>
|
||||
{file.name}
|
||||
</Trim>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCellText
|
||||
title="ID"
|
||||
onlyDesktop>
|
||||
<Id value={file.$id}
|
||||
>{file.$id}</Id>
|
||||
</TableCellText>
|
||||
<TableCellText
|
||||
title="Type"
|
||||
onlyDesktop>
|
||||
{file.mimeType}
|
||||
</TableCellText>
|
||||
<TableCellText
|
||||
title="Size"
|
||||
onlyDesktop>
|
||||
{calculateSize(
|
||||
file.sizeOriginal
|
||||
)}
|
||||
</TableCellText>
|
||||
<TableCellText
|
||||
title="Created"
|
||||
onlyDesktop>
|
||||
{toLocaleDate(
|
||||
file.$createdAt
|
||||
)}
|
||||
</TableCellText>
|
||||
</TableRowButton>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
{:else if $search}
|
||||
<article
|
||||
style:--card-bg-color="transparent"
|
||||
style:--shadow-small="none"
|
||||
style:--color-border="var(--color-neutral-15)"
|
||||
class="card u-grid u-cross-center u-width-full-line common-section is-border-dashed">
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-overflow-hidden">
|
||||
<div class="common-section">
|
||||
<div
|
||||
class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold"
|
||||
>Sorry we couldn't find "{$search}"</b>
|
||||
<p>
|
||||
There are no files that match
|
||||
your search.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => ($search = '')}
|
||||
>Clear search</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
noMedia
|
||||
--card-bg-color="transparent"
|
||||
--shadow-small="none"
|
||||
--color-border="var(--color-neutral-15)">
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<Heading
|
||||
size="7"
|
||||
tag="h2"
|
||||
trimmed={false}>
|
||||
No files found within this bucket.
|
||||
</Heading>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Learn more in our <a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/products/storage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
noMedia
|
||||
--card-bg-color="transparent"
|
||||
--shadow-small="none"
|
||||
--color-border="var(--color-neutral-15)">
|
||||
<div class="u-text-center u-flex-vertical u-cross-center u-gap-24">
|
||||
<Heading size="7" tag="h2" trimmed={false}>
|
||||
No buckets found
|
||||
</Heading>
|
||||
<Button
|
||||
secondary
|
||||
external
|
||||
href={`${base}/console/project-${$page.params.project}/storage`}>
|
||||
Create bucket
|
||||
</Button>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
{/await}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer u-margin-block-start-0">
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<Button text on:click={closeModal}>Cancel</Button>
|
||||
<Button submit disabled={selectedBucket === null || selectedFile === null}
|
||||
>Select</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</ModalWrapper>
|
||||
|
||||
<style lang="scss">
|
||||
input[type='radio']:where(:indeterminate) {
|
||||
--p-bg-color: var(--p-bg-color-default);
|
||||
--p-border-color: var(--p-border-color-default);
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { Button, InputNumber } from '$lib/elements/forms';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import InputText from '$lib/elements/forms/inputText.svelte';
|
||||
import {
|
||||
Button,
|
||||
InputNumber,
|
||||
InputSelect,
|
||||
InputText,
|
||||
InputTags,
|
||||
FormList,
|
||||
InputSelectCheckbox
|
||||
} from '$lib/elements/forms';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { tags, type Operator, queries } from './store';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { tags, type Operator, queries, type TagValue } from './store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
export let columns: Writable<Column[]>;
|
||||
export let columnId: string | null = null;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
clear: void;
|
||||
apply: { applied: number };
|
||||
}>();
|
||||
|
||||
let columnId: string | null = null;
|
||||
$: column = $columns.find((c) => c.id === columnId) as Column;
|
||||
let arrayValues: string[] = [];
|
||||
|
||||
dispatch('apply', { applied: $tags.length });
|
||||
|
||||
@@ -36,7 +44,7 @@
|
||||
toTag: (attribute, input) => `**${attribute}** greater than **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'greater than or equal to': {
|
||||
'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']
|
||||
@@ -46,7 +54,7 @@
|
||||
toTag: (attribute, input) => `**${attribute}** less than **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
},
|
||||
'less than or equal to': {
|
||||
'less than or equal': {
|
||||
toQuery: Query.lessThanEqual,
|
||||
toTag: (attribute, input) => `**${attribute}** less than or equal to **${input}**`,
|
||||
types: ['integer', 'double', 'datetime']
|
||||
@@ -56,22 +64,36 @@
|
||||
toTag: (attribute, input) => `**${attribute}** equal to **${input}**`,
|
||||
types: ['string', 'integer', 'double', 'boolean']
|
||||
},
|
||||
'not equal to': {
|
||||
'not equal': {
|
||||
toQuery: Query.notEqual,
|
||||
toTag: (attribute, input) => `**${attribute}** not equal to **${input}**`,
|
||||
types: ['string', 'integer', 'double', 'boolean']
|
||||
},
|
||||
'not null': {
|
||||
'is not null': {
|
||||
toQuery: Query.isNotNull,
|
||||
toTag: (attribute) => `**${attribute}** is not null`,
|
||||
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'],
|
||||
hideInput: true
|
||||
},
|
||||
null: {
|
||||
'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']
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,23 +115,48 @@
|
||||
/* eslint @typescript-eslint/no-explicit-any: 'off' */
|
||||
let value: any = null;
|
||||
|
||||
$: {
|
||||
columnId;
|
||||
value = null;
|
||||
}
|
||||
onMount(() => {
|
||||
value = column?.array ? [] : null;
|
||||
});
|
||||
|
||||
// This Map is keyed by tags, and has a query as the value
|
||||
function addFilter() {
|
||||
if (!column || !operator) return;
|
||||
|
||||
queries.addFilter({ column, operator, value: value ?? '' });
|
||||
value = null;
|
||||
if (column.array) {
|
||||
queries.addFilter({ column, operator, value: arrayValues });
|
||||
columnId = null;
|
||||
arrayValues = [];
|
||||
} else {
|
||||
queries.addFilter({ column, operator, value: value ?? '' });
|
||||
columnId = null;
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function tagFormat(node: HTMLElement) {
|
||||
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||
}
|
||||
|
||||
function formatArray(array: string[]) {
|
||||
if (!array?.length) return;
|
||||
if (array.length > 2) {
|
||||
return `${array[0]} or ${array.length - 1} others`;
|
||||
} else {
|
||||
return array.join(' or ');
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
$: isDisabled = !operator;
|
||||
</script>
|
||||
|
||||
@@ -118,10 +165,12 @@
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<InputSelect
|
||||
id="column"
|
||||
options={$columns.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id
|
||||
}))}
|
||||
options={$columns
|
||||
.filter((c) => c.filter !== false)
|
||||
.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id
|
||||
}))}
|
||||
placeholder="Select column"
|
||||
bind:value={columnId} />
|
||||
<InputSelect
|
||||
@@ -132,23 +181,55 @@
|
||||
bind:value={operatorKey} />
|
||||
</ul>
|
||||
{#if column && operator && !operator?.hideInput}
|
||||
<div class="u-margin-block-start-8">
|
||||
{#if column.type === 'integer' || column.type === 'double'}
|
||||
<InputNumber id="value" bind:value placeholder="Enter value" />
|
||||
{:else if column.type === 'boolean'}
|
||||
<InputSelect
|
||||
id="value"
|
||||
placeholder="Select a value"
|
||||
required={true}
|
||||
options={[
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false }
|
||||
].filter(Boolean)}
|
||||
bind:value />
|
||||
{:else}
|
||||
<InputText id="value" bind:value placeholder="Enter value" />
|
||||
{/if}
|
||||
</div>
|
||||
{#if column?.array}
|
||||
<FormList class="u-margin-block-start-8">
|
||||
{#if column.format === 'enum'}
|
||||
<InputSelectCheckbox
|
||||
name="value"
|
||||
bind:tags={arrayValues}
|
||||
placeholder="Select value"
|
||||
options={column?.elements?.map((value) => ({
|
||||
label: value,
|
||||
value,
|
||||
checked: arrayValues.includes(value)
|
||||
}))}>
|
||||
</InputSelectCheckbox>
|
||||
{:else}
|
||||
<InputTags
|
||||
label="values"
|
||||
showLabel={false}
|
||||
id="value"
|
||||
bind:tags={arrayValues}
|
||||
placeholder="Enter values" />
|
||||
{/if}
|
||||
</FormList>
|
||||
{:else}
|
||||
<ul class="u-margin-block-start-8">
|
||||
{#if column.format === 'enum'}
|
||||
<InputSelect
|
||||
id="value"
|
||||
bind:value
|
||||
placeholder="Select value"
|
||||
options={column?.elements?.map((value) => ({ label: value, value }))}
|
||||
label="Value"
|
||||
showLabel={false} />
|
||||
{:else if column.type === 'integer' || column.type === 'double'}
|
||||
<InputNumber id="value" bind:value placeholder="Enter value" />
|
||||
{:else if column.type === 'boolean'}
|
||||
<InputSelect
|
||||
id="value"
|
||||
placeholder="Select a value"
|
||||
required={true}
|
||||
options={[
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false }
|
||||
].filter(Boolean)}
|
||||
bind:value />
|
||||
{:else}
|
||||
<InputText id="value" bind:value placeholder="Enter value" />
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button text disabled={isDisabled} class="u-margin-block-start-4" submit>
|
||||
<i class="icon-plus" />
|
||||
@@ -158,16 +239,32 @@
|
||||
|
||||
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
|
||||
{#each $tags as tag (tag)}
|
||||
<button
|
||||
class="tag"
|
||||
on:click={() => {
|
||||
queries.removeFilter(tag);
|
||||
}}>
|
||||
<span class="text" use:tagFormat>
|
||||
{tag}
|
||||
</span>
|
||||
<i class="icon-x" />
|
||||
</button>
|
||||
{#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>
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
|
||||
export let query = '[]';
|
||||
export let columns: Writable<Column[]>;
|
||||
export let disabled = false;
|
||||
export let fullWidthMobile = false;
|
||||
|
||||
const parsedQueries = queryParamToMap(query);
|
||||
queries.set(parsedQueries);
|
||||
|
||||
let selectedColumn: string | null = null;
|
||||
// We need to separate them so we don't trigger Drop's handlers
|
||||
let showFiltersDesktop = false;
|
||||
let showFiltersMobile = false;
|
||||
@@ -24,11 +27,20 @@
|
||||
showFiltersDesktop = false;
|
||||
showFiltersMobile = false;
|
||||
});
|
||||
|
||||
function clearAll() {
|
||||
selectedColumn = null;
|
||||
queries.clearAll();
|
||||
}
|
||||
|
||||
$: if (!showFiltersDesktop && !showFiltersMobile) {
|
||||
selectedColumn = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="is-not-mobile">
|
||||
<Drop bind:show={showFiltersDesktop} noArrow>
|
||||
<Button secondary on:click={() => (showFiltersDesktop = !showFiltersDesktop)}>
|
||||
<Button secondary on:click={() => (showFiltersDesktop = !showFiltersDesktop)} {disabled}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
@@ -41,12 +53,13 @@
|
||||
<div class="dropped card">
|
||||
<p>Apply filter rules to refine the table view</p>
|
||||
<Content
|
||||
bind:columnId={selectedColumn}
|
||||
{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={queries.clearAll}>Clear all</Button>
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
<Button on:click={queries.apply} disabled={!$queriesAreDirty}>Apply</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,7 +68,7 @@
|
||||
</div>
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)}>
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)} {fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
@@ -75,7 +88,7 @@
|
||||
on:apply={(e) => (applied = e.detail.applied)}
|
||||
on:clear={() => (applied = 0)} />
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={queries.clearAll}>Clear all</Button>
|
||||
<Button text on:click={clearAll}>Clear all</Button>
|
||||
<Button on:click={queries.apply} disabled={!$queriesAreDirty}>Apply</Button
|
||||
></svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as filters } from './filters.svelte';
|
||||
export { default as Filters } from './filters.svelte';
|
||||
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
|
||||
|
||||
@@ -5,14 +5,19 @@ import { page } from '$app/stores';
|
||||
import deepEqual from 'deep-equal';
|
||||
import type { Column, ColumnType } from '$lib/helpers/types';
|
||||
|
||||
export type TagValue = {
|
||||
tag: string;
|
||||
value: string | number | string[];
|
||||
};
|
||||
|
||||
export type Operator = {
|
||||
toTag: (attribute: string, input?: string | number) => string;
|
||||
toQuery: (attribute: string, input?: string | number) => string;
|
||||
toTag: (attribute: string, input?: string | number | string[]) => string | TagValue;
|
||||
toQuery: (attribute: string, input?: string | number | string[]) => string;
|
||||
types: ColumnType[];
|
||||
hideInput?: boolean;
|
||||
};
|
||||
|
||||
export function mapToQueryParams(map: Map<string, string>) {
|
||||
export function mapToQueryParams(map: Map<string | TagValue, string>) {
|
||||
return encodeURIComponent(JSON.stringify(Array.from(map.entries())));
|
||||
}
|
||||
|
||||
@@ -22,13 +27,13 @@ export function queryParamToMap(queryParam: string) {
|
||||
return new Map(queries);
|
||||
}
|
||||
|
||||
function initQueries(initialValue = new Map<string, string>()) {
|
||||
function initQueries(initialValue = new Map<string | TagValue, string>()) {
|
||||
const queries = writable(initialValue);
|
||||
|
||||
type AddFilterArgs = {
|
||||
operator: Operator;
|
||||
column: Column;
|
||||
value: string | number;
|
||||
value: string | number | string[];
|
||||
};
|
||||
|
||||
function addFilter({ column, operator, value }: AddFilterArgs) {
|
||||
@@ -38,7 +43,7 @@ function initQueries(initialValue = new Map<string, string>()) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeFilter(tag: string) {
|
||||
function removeFilter(tag: string | TagValue) {
|
||||
queries.update((map) => {
|
||||
map.delete(tag);
|
||||
return map;
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
export let id: string = null;
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
export let style = '';
|
||||
</script>
|
||||
|
||||
<svelte:element this={tag} class={`heading-level-${size} ${classes}`} class:u-trim-1={trimmed} {id}>
|
||||
<svelte:element
|
||||
this={tag}
|
||||
class={`heading-level-${size} u-min-width-0 ${classes}`}
|
||||
class:u-trim-1={trimmed}
|
||||
{style}
|
||||
{id}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Copy {value} {event}>
|
||||
<Copy {value} {event} appendTo="parent">
|
||||
<div
|
||||
class="interactive-text-output is-buttons-on-top"
|
||||
class:u-text-center={centered}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { app } from '$lib/stores/app';
|
||||
|
||||
export let darkSrc: string;
|
||||
export let lightSrc: string;
|
||||
export let alt: string;
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={$app.themeInUse === 'dark' ? darkSrc : lightSrc}
|
||||
class="file-preview is-with-image"
|
||||
style="inline-size: 100%; block-size: 100%;"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="open file in new window">
|
||||
<div class="file-preview-image">
|
||||
<img src={$app.themeInUse === 'dark' ? darkSrc : lightSrc} {alt} />
|
||||
</div>
|
||||
<div class="file-preview-content">
|
||||
<div class="avatar">
|
||||
<span class="icon-external-link" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -15,6 +15,7 @@ export { default as UploadBox } from './uploadBox.svelte';
|
||||
export { default as List } from './list.svelte';
|
||||
export { default as ListItem } from './listItem.svelte';
|
||||
export { default as Empty } from './empty.svelte';
|
||||
export { default as EmptyFilter } from './emptyFilter.svelte';
|
||||
export { default as EmptySearch } from './emptySearch.svelte';
|
||||
export { default as Drop } from './drop.svelte';
|
||||
export { default as DropList } from './dropList.svelte';
|
||||
@@ -71,3 +72,5 @@ export { default as FakeModal } from './fakeModal.svelte';
|
||||
export { default as RadioBoxes } from './radioBoxes.svelte';
|
||||
export { default as ModalWrapper } from './modalWrapper.svelte';
|
||||
export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export let name: string;
|
||||
export let group: string;
|
||||
@@ -7,6 +9,7 @@
|
||||
export let disabled = false;
|
||||
export let padding = 1;
|
||||
export let icon: string = null;
|
||||
export let imageIcon: string = null;
|
||||
export let fullHeight = true;
|
||||
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
|
||||
export let backgroundColor: string = null;
|
||||
@@ -57,6 +60,14 @@
|
||||
{#if icon}
|
||||
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
|
||||
{/if}
|
||||
{#if imageIcon}
|
||||
<img
|
||||
class="u-margin-inline-start-auto"
|
||||
style:max-inline-size="1.25rem"
|
||||
style:max-block-size="1.25rem"
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${imageIcon}.svg`}
|
||||
alt={imageIcon} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<script context="module" lang="ts">
|
||||
export async function verify(challenge: Models.MfaChallenge, code: string) {
|
||||
try {
|
||||
if (challenge == null) {
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(
|
||||
AuthenticationFactor.Totp
|
||||
);
|
||||
}
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.AccountCreate);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Button, FormItem, FormList, InputDigits, InputText } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
|
||||
export let factors: Models.MfaFactors & { recoveryCode: boolean };
|
||||
/** If true, the form will be submitted automatically when the code is entered. */
|
||||
export let autoSubmit: boolean = true;
|
||||
export let showVerifyButton: boolean = true;
|
||||
export let disabled: boolean = false;
|
||||
export let challenge: Models.MfaChallenge;
|
||||
export let code: string;
|
||||
|
||||
let challengeType: AuthenticationFactor;
|
||||
|
||||
const enabledFactors = Object.entries(factors).filter(([_, enabled]) => enabled);
|
||||
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
disabled = true;
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const enabledNonRecoveryFactors = enabledFactors.filter(
|
||||
([factor, _]) => factor != 'recoveryCode'
|
||||
);
|
||||
if (enabledNonRecoveryFactors.length == 1) {
|
||||
if (factors.phone) {
|
||||
createChallenge(AuthenticationFactor.Phone);
|
||||
} else if (factors.email) {
|
||||
createChallenge(AuthenticationFactor.Email);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<FormList>
|
||||
{#if challengeType == AuthenticationFactor.Recoverycode}
|
||||
<p>
|
||||
Enter below one of the recovery codes you received when enabling MFA for this account.
|
||||
</p>
|
||||
<InputText id="recovery-code" bind:value={code} required autofocus />
|
||||
{:else}
|
||||
{#if factors.totp && (challengeType == AuthenticationFactor.Totp || challengeType == null)}
|
||||
<p>Enter below a 6-digit one-time code generated by your authentication app.</p>
|
||||
{:else if challengeType == AuthenticationFactor.Email}
|
||||
<p>A 6-digit verification code was sent to your email, enter it below.</p>
|
||||
{: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} />
|
||||
{/if}
|
||||
{#if showVerifyButton}
|
||||
<FormItem>
|
||||
<Button fullWidth submit {disabled}>Verify</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if enabledFactors.length > 1}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
{#if factors.totp && challengeType != null && challengeType != AuthenticationFactor.Totp}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Totp)}>
|
||||
<span class="icon-device-mobile u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Authenticator app</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.email && challengeType != AuthenticationFactor.Email}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Email)}>
|
||||
<span class="icon-mail u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Email verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.phone && challengeType != AuthenticationFactor.Phone}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Phone)}>
|
||||
<span class="icon-chat-alt u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Phone verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.recoveryCode && challengeType != AuthenticationFactor.Recoverycode}
|
||||
<FormItem>
|
||||
<Button
|
||||
text
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Recoverycode)}>
|
||||
<span class="text">Use recovery code</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
@@ -5,7 +5,7 @@
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' = null;
|
||||
export let size: 'small' | 'big' | 'huge' = null;
|
||||
export let icon: string = null;
|
||||
export let state: 'success' | 'warning' | 'error' | 'info' = null;
|
||||
export let error: string = null;
|
||||
@@ -64,11 +64,13 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="u-margin-block-start-4">
|
||||
<slot name="description">
|
||||
{description}
|
||||
</slot>
|
||||
</p>
|
||||
{#if description.length > 0}
|
||||
<p class="u-margin-block-start-4">
|
||||
<slot name="description">
|
||||
{description}
|
||||
</slot>
|
||||
</p>
|
||||
{/if}
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' = null;
|
||||
export let size: 'small' | 'big' | 'huge' = null;
|
||||
export let closable = true;
|
||||
export let headerDivider = true;
|
||||
export let style = '';
|
||||
@@ -70,13 +70,30 @@
|
||||
|
||||
<dialog
|
||||
class="modal"
|
||||
class:u-hide={!show}
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class:is-huge={size === 'huge'}
|
||||
class:is-separate-header={headerDivider}
|
||||
{style}
|
||||
bind:this={dialog}
|
||||
on:cancel|preventDefault>
|
||||
on:cancel|preventDefault
|
||||
{style}>
|
||||
{#if show}
|
||||
<slot close={closeModal} />
|
||||
{/if}
|
||||
</dialog>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
.modal.is-huge {
|
||||
block-size: 100%;
|
||||
min-block-size: 80vh;
|
||||
|
||||
@media #{$break1}, #{$break2} {
|
||||
min-inline-size: 100%;
|
||||
min-block-size: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export let items = [];
|
||||
export let limit = 5;
|
||||
export let hideFooter = false;
|
||||
|
||||
let offset = 0;
|
||||
|
||||
@@ -10,9 +11,11 @@
|
||||
$: paginatedItems = items.slice(offset, offset + limit);
|
||||
</script>
|
||||
|
||||
<slot {paginatedItems} />
|
||||
<slot {paginatedItems} {limit} />
|
||||
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {total}</p>
|
||||
<PaginationInline {limit} bind:offset sum={total} hidePages />
|
||||
</div>
|
||||
{#if !hideFooter}
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {total}</p>
|
||||
<PaginationInline {limit} bind:offset sum={total} hidePages />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -118,8 +118,8 @@
|
||||
<!-- svelte-ignore css-unused-selector -->
|
||||
<style lang="scss" global>
|
||||
.tippy-user .tippy-box {
|
||||
--p-drop-bg-color: var(--color-neutral-500);
|
||||
--p-drop-border-color: var(--color-neutral-200);
|
||||
--p-drop-bg-color: var(--color-neutral-105);
|
||||
--p-drop-border-color: var(--color-neutral-85);
|
||||
|
||||
body.theme-light & {
|
||||
--p-drop-bg-color: var(--color-neutral-0);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Permission } from './permissions.svelte';
|
||||
import Table from '$lib/elements/table/table.svelte';
|
||||
import { TableBody, TableCell, TableRow } from '$lib/elements/table';
|
||||
|
||||
export let show: boolean;
|
||||
export let groups: Writable<Map<string, Permission>>;
|
||||
@@ -77,13 +79,13 @@
|
||||
bind:value={search} />
|
||||
{#if results?.teams?.length}
|
||||
<div class="table-wrapper">
|
||||
<table class="table is-table-layout-auto is-remove-outer-styles">
|
||||
<tbody class="table-tbody">
|
||||
<Table noStyles isAutoLayout tag="table">
|
||||
<TableBody>
|
||||
{#each results.teams as team (team.$id)}
|
||||
{@const role = `team:${team.$id}`}
|
||||
{@const exists = $groups.has(role)}
|
||||
<tr class="table-row">
|
||||
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
|
||||
<TableRow>
|
||||
<TableCell title="Enabled" width={40}>
|
||||
<input
|
||||
id={team.$id}
|
||||
type="checkbox"
|
||||
@@ -92,8 +94,8 @@
|
||||
checked={exists || selected.has(role)}
|
||||
disabled={exists}
|
||||
on:change={(event) => onSelection(event, role)} />
|
||||
</td>
|
||||
<td class="table-col" data-title="Team">
|
||||
</TableCell>
|
||||
<TableCell title="Team">
|
||||
<label class="u-flex u-cross-center u-gap-8" for={team.$id}>
|
||||
<AvatarInitials size={32} name={team.name} />
|
||||
<div class="u-line-height-1-5">
|
||||
@@ -101,11 +103,11 @@
|
||||
<div class="u-x-small">{team.$id}</div>
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {results?.total}</p>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
export let disabled = false;
|
||||
export let autofocus = false;
|
||||
export let isWithEndButton = true;
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
@@ -58,7 +59,7 @@
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<div class={fullWidth ? 'u-width-full-line' : 'u-flex-basis-50-percent'}>
|
||||
<div class="input-text-wrapper" class:is-with-end-button={isWithEndButton}>
|
||||
<input
|
||||
{placeholder}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
export let large = false;
|
||||
export let stretch = false;
|
||||
let classes: string = undefined;
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
<ul class="secondary-tabs {classes}" class:is-large={large}>
|
||||
<ul class="secondary-tabs {classes}" class:is-large={large} class:is-stretch={stretch}>
|
||||
<slot />
|
||||
</ul>
|
||||
|
||||
@@ -1,55 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import SupportOfflineLight from '$lib/images/support/support-offline-light.png';
|
||||
import SupportOfflineDark from '$lib/images/support/support-offline-dark.png';
|
||||
import SupportOnlineLight from '$lib/images/support/support-online-light.png';
|
||||
import SupportOnlineDark from '$lib/images/support/support-online-dark.png';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import SupportWizard from '../../routes/console/supportWizard.svelte';
|
||||
import { isSupportOnline, showSupportModal } from '../../routes/console/wizard/support/store';
|
||||
import { showSupportModal } from '../../routes/console/wizard/support/store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { localeTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: isPaid =
|
||||
$organization?.billingPlan === BillingPlan.PRO ||
|
||||
$organization?.billingPlan === BillingPlan.SCALE;
|
||||
|
||||
$: supportTimings = `${utcHourToLocaleHour('09:00')} - ${utcHourToLocaleHour('17:00')} ${localeTimezoneName()}`;
|
||||
</script>
|
||||
|
||||
{#if isCloud}
|
||||
<section class="drop-section u-grid u-gap-24 u-padding-24">
|
||||
<div class="u-flex u-main-center">
|
||||
{#if isSupportOnline()}
|
||||
{#if $app.themeInUse === 'light'}
|
||||
<img src={SupportOnlineLight} alt="Support" height="87" width="230" />
|
||||
{:else}
|
||||
<img src={SupportOnlineDark} alt="Support" height="87" width="230" />
|
||||
{/if}
|
||||
{:else if $app.themeInUse === 'light'}
|
||||
<img src={SupportOfflineLight} alt="Support" height="87" width="230" />
|
||||
{:else}
|
||||
<img src={SupportOfflineDark} alt="Support" height="87" width="230" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="eyebrow-heading-3">Premium support</h4>
|
||||
{#if isPaid}
|
||||
<p class="u-line-height-1-5 u-margin-block-start-8">
|
||||
Get personalized support from the Appwrite team from <b>09:00 - 17:00 UTC</b>
|
||||
Get personalized support from the Appwrite team from <b>{supportTimings}</b>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
<Button
|
||||
fullWidth
|
||||
external
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'support_menu'
|
||||
@@ -78,6 +63,7 @@
|
||||
<div class="u-margin-block-start-8 u-width-full-line">
|
||||
{#key $app.themeInUse}
|
||||
<iframe
|
||||
style="color-scheme: none"
|
||||
title="Appwrite Status"
|
||||
src={`https://status.appwrite.online/badge?theme=${
|
||||
$app.themeInUse === 'dark' ? 'dark' : 'light'
|
||||
|
||||
@@ -16,16 +16,13 @@
|
||||
|
||||
<svelte:window on:resize={throttle(onResize, 250)} />
|
||||
|
||||
<span class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`} bind:this={container}>
|
||||
{#if showTooltip}
|
||||
<span
|
||||
use:tooltip={{
|
||||
content: container.innerText,
|
||||
maxWidth: '30rem'
|
||||
}}>
|
||||
<slot />
|
||||
</span>
|
||||
{:else}
|
||||
<span><slot /></span>
|
||||
{/if}
|
||||
<span
|
||||
class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`}
|
||||
bind:this={container}
|
||||
use:tooltip={{
|
||||
disabled: !showTooltip,
|
||||
content: container?.innerText ?? undefined,
|
||||
maxWidth: '30rem'
|
||||
}}>
|
||||
<span><slot /></span>
|
||||
</span>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let hideColumns = false;
|
||||
export let allowNoColumns = false;
|
||||
export let showColsTextMobile = false;
|
||||
export let fullWidthMobile = false;
|
||||
|
||||
let showSelectColumns = false;
|
||||
|
||||
@@ -31,12 +32,15 @@
|
||||
} else {
|
||||
const prefs = preferences.get($page.route);
|
||||
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.show = prefs.columns?.includes(column.id) ?? true;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
// Override the shown columns only if a preference was set
|
||||
if (prefs?.columns) {
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.show = prefs.columns?.includes(column.id) ?? true;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
columns.subscribe((ctx) => {
|
||||
@@ -71,8 +75,11 @@
|
||||
<div class="grid-header-col-4">
|
||||
{#if !hideColumns && view === View.Table}
|
||||
{#if $columns?.length}
|
||||
<DropList bind:show={showSelectColumns} scrollable>
|
||||
<Button secondary on:click={() => (showSelectColumns = !showSelectColumns)}>
|
||||
<DropList bind:show={showSelectColumns} scrollable wrapperFullWidth={fullWidthMobile}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showSelectColumns = !showSelectColumns)}
|
||||
{fullWidthMobile}>
|
||||
<span
|
||||
class="icon-view-boards u-opacity-50"
|
||||
aria-hidden="true"
|
||||
|
||||
+124
-2
@@ -3,6 +3,8 @@ export const CARD_LIMIT = 6; // default card limit
|
||||
export const INTERVAL = 5 * 60000; // default interval to check for feedback
|
||||
|
||||
export enum Dependencies {
|
||||
FACTORS = 'dependency:factors',
|
||||
IDENTITIES = 'dependency:identities',
|
||||
CREDIT = 'dependency:credit',
|
||||
INVOICES = 'dependency:invoices',
|
||||
ADDRESS = 'dependency:address',
|
||||
@@ -17,6 +19,8 @@ export enum Dependencies {
|
||||
ACCOUNT_SESSIONS = 'dependency:account_sessions',
|
||||
USER = 'dependency:user',
|
||||
USERS = 'dependency:users',
|
||||
USER_IDENTITIES = 'dependency:user_identities',
|
||||
USER_TARGETS = 'dependency:user_targets',
|
||||
SESSIONS = 'dependency:sessions',
|
||||
TEAM = 'dependency:team',
|
||||
TEAMS = 'dependency:teams',
|
||||
@@ -47,7 +51,14 @@ export enum Dependencies {
|
||||
MIGRATIONS = 'dependency:migrations',
|
||||
COLLECTIONS = 'dependency:collections',
|
||||
RUNTIMES = 'dependency:runtimes',
|
||||
CONSOLE_VARIABLES = 'dependency:console_variables'
|
||||
CONSOLE_VARIABLES = 'dependency:console_variables',
|
||||
MESSAGING_PROVIDERS = 'dependency:messaging_providers',
|
||||
MESSAGING_PROVIDER = 'dependency:messaging_provider',
|
||||
MESSAGING_MESSAGES = 'dependency:messaging_messages',
|
||||
MESSAGING_MESSAGE = 'dependency:messaging_message',
|
||||
MESSAGING_TOPICS = 'dependency:messaging_topics',
|
||||
MESSAGING_TOPIC = 'dependency:messaging_topic',
|
||||
MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
|
||||
}
|
||||
|
||||
export const scopes: {
|
||||
@@ -55,6 +66,11 @@ export const scopes: {
|
||||
description: string;
|
||||
category: string;
|
||||
}[] = [
|
||||
{
|
||||
scope: 'sessions.write',
|
||||
description: "Access to create, update and delete your project's sessions",
|
||||
category: 'Auth'
|
||||
},
|
||||
{
|
||||
scope: 'users.read',
|
||||
description: "Access to read your project's users",
|
||||
@@ -168,6 +184,57 @@ export const scopes: {
|
||||
description: "Access to execute your project's functions",
|
||||
category: 'Functions'
|
||||
},
|
||||
{
|
||||
scope: 'targets.read',
|
||||
description: "Access to read your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'targets.write',
|
||||
description: "Access to create, update, and delete your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'providers.read',
|
||||
description: "Access to read your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'providers.write',
|
||||
description: "Access to create, update, and delete your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'messages.read',
|
||||
description: "Access to read your project's messages",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'messages.write',
|
||||
description: "Access to create, update, and delete your project's messages",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'topics.read',
|
||||
description: "Access to read your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'topics.write',
|
||||
description: "Access to create, update, and delete your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.read',
|
||||
description: "Access to read your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'locale.read',
|
||||
description: "Access to access your project's Locale service",
|
||||
@@ -276,11 +343,66 @@ export const eventServices: Array<EventService> = [
|
||||
{ name: 'update', attributes: ['email', 'name', 'password', 'status', 'prefs'] },
|
||||
{ name: 'delete' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'providers',
|
||||
resources: [],
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'topics',
|
||||
resources: [
|
||||
{
|
||||
name: 'subscribers',
|
||||
actions: [{ name: 'create' }, { name: 'delete' }]
|
||||
}
|
||||
],
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
},
|
||||
{
|
||||
name: 'messages',
|
||||
resources: [],
|
||||
actions: [{ name: 'create' }, { name: 'update' }, { name: 'delete' }]
|
||||
}
|
||||
];
|
||||
|
||||
export enum BillingPlan {
|
||||
STARTER = 'tier-0',
|
||||
FREE = 'tier-0',
|
||||
PRO = 'tier-1',
|
||||
SCALE = 'tier-2'
|
||||
}
|
||||
|
||||
export const feedbackDowngradeOptions = [
|
||||
{
|
||||
value: 'availableFeatures',
|
||||
label: "The available features don't meet my needs"
|
||||
},
|
||||
{
|
||||
value: 'traction',
|
||||
label: "My project isn't getting traction"
|
||||
},
|
||||
{
|
||||
value: 'bugs',
|
||||
label: 'I experienced bugs or unexpected outages while using the console'
|
||||
},
|
||||
{
|
||||
value: 'starter',
|
||||
label: 'The Free plan is enough for my projects'
|
||||
},
|
||||
{
|
||||
value: 'budget',
|
||||
label: "I don't have the budget"
|
||||
},
|
||||
{
|
||||
value: 'tryOut',
|
||||
label: 'I just wanted to try it out'
|
||||
},
|
||||
{
|
||||
value: 'alternative',
|
||||
label: 'I found an alternative/competitor to meet my needs'
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
label: 'Other'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Flag } from '@appwrite.io/console';
|
||||
export let flag: string;
|
||||
export let name: string = flag;
|
||||
export let width = 40;
|
||||
@@ -9,6 +11,7 @@
|
||||
export { classes as class };
|
||||
|
||||
export function getFlag(country: string, width: number, height: number, quality: number) {
|
||||
if (!isValueOfStringEnum(Flag, country)) return '';
|
||||
let flag = sdk.forProject.avatars
|
||||
.getFlag(country, width * 2, height * 2, quality)
|
||||
?.toString();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let disabled = false;
|
||||
export let external = false;
|
||||
export let href: string = null;
|
||||
export let download: string = undefined;
|
||||
export let fullWidth = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let ariaLabel: string = null;
|
||||
@@ -63,6 +64,7 @@
|
||||
on:click
|
||||
on:click={track}
|
||||
{href}
|
||||
{download}
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class={resolvedClasses}
|
||||
|
||||
@@ -17,10 +17,19 @@
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
|
||||
const { isSubmitting } = setContext<FormContext>('form', {
|
||||
let form: HTMLFormElement;
|
||||
|
||||
export let { isSubmitting } = setContext<FormContext>('form', {
|
||||
isSubmitting: writable(false)
|
||||
});
|
||||
|
||||
export function checkValidity() {
|
||||
return form.checkValidity();
|
||||
}
|
||||
export function triggerSubmit() {
|
||||
form.requestSubmit();
|
||||
}
|
||||
|
||||
async function submit(e: SubmitEvent) {
|
||||
isSubmitting.set(true);
|
||||
await onSubmit(e);
|
||||
@@ -29,6 +38,7 @@
|
||||
</script>
|
||||
|
||||
<form
|
||||
bind:this={form}
|
||||
class={classes}
|
||||
class:form={!noStyle}
|
||||
class:common-section={!noMargin}
|
||||
|
||||
@@ -29,3 +29,7 @@ export { default as Label } from './label.svelte';
|
||||
export { default as InputProjectId } from './inputProjectId.svelte';
|
||||
export { default as InputDate } from './inputDate.svelte';
|
||||
export { default as InputDateRange } from './inputDateRange.svelte';
|
||||
export { default as InputTime } from './inputTime.svelte';
|
||||
export { default as InputDigits } from './inputDigits.svelte';
|
||||
export { default as InputSelectCheckbox } from './inputSelectCheckbox.svelte';
|
||||
export { default as InputFilePicker } from './inputFilePicker.svelte';
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { FormItem, Helper } from '.';
|
||||
import type { FormItemTag } from './formItem.svelte';
|
||||
|
||||
interface $$Props extends Partial<HTMLLabelElement> {
|
||||
id: string;
|
||||
label?: string;
|
||||
optionalText?: string;
|
||||
tooltip?: string;
|
||||
showLabel?: boolean;
|
||||
checked?: boolean;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
element?: HTMLInputElement | undefined;
|
||||
indeterminate?: boolean;
|
||||
wrapperTag?: FormItemTag;
|
||||
size?: 'small' | 'medium';
|
||||
}
|
||||
|
||||
export let id: string;
|
||||
export let label: string | undefined = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let tooltip: string = null;
|
||||
export let showLabel = true;
|
||||
export let checked = false;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let element: HTMLInputElement | undefined = undefined;
|
||||
export let wrapperTag: FormItemTag = 'li';
|
||||
|
||||
export let size: $$Props['size'] = 'medium';
|
||||
let error: string;
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
@@ -44,25 +39,28 @@
|
||||
</script>
|
||||
|
||||
<FormItem tag={wrapperTag}>
|
||||
{#if label}
|
||||
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
{...$$restProps}
|
||||
type="checkbox"
|
||||
bind:this={element}
|
||||
bind:checked
|
||||
on:invalid={handleInvalid}
|
||||
on:click
|
||||
on:change />
|
||||
</div>
|
||||
<label class="choice-item" for={id}>
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
{...$$restProps}
|
||||
class:is-small={size === 'small'}
|
||||
type="checkbox"
|
||||
bind:this={element}
|
||||
bind:checked
|
||||
on:invalid={handleInvalid}
|
||||
on:click
|
||||
on:change />
|
||||
</div>
|
||||
<div class="choice-item-content">
|
||||
{#if label}
|
||||
<div class="choice-item-title">{label}</div>
|
||||
{/if}
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</label>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { tooltip as tooltipAction } from '$lib/actions/tooltip';
|
||||
import { FormItem, Helper } from '.';
|
||||
|
||||
export let type: 'checkbox' | 'switchbox' = 'checkbox';
|
||||
@@ -39,6 +40,7 @@
|
||||
aria-checked={value}
|
||||
bind:this={element}
|
||||
bind:checked={value}
|
||||
on:change
|
||||
on:invalid={handleInvalid} />
|
||||
|
||||
<div class="choice-item-content" class:u-width-full-line={fullWidth}>
|
||||
@@ -50,16 +52,15 @@
|
||||
</h6>
|
||||
{/if}
|
||||
{#if tooltip}
|
||||
<button type="button" class="tooltip" aria-label="variables info">
|
||||
<button
|
||||
type="button"
|
||||
class="tooltip"
|
||||
aria-label="variables info"
|
||||
use:tooltipAction={{ content: tooltip }}>
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
<span class="tooltip-popup" role="tooltip">
|
||||
<p class="text">
|
||||
{tooltip}
|
||||
</p>
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
export let nullable = false;
|
||||
export let min: string | number | undefined = undefined;
|
||||
export let max: string | number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
@@ -65,12 +67,18 @@
|
||||
{readonly}
|
||||
{required}
|
||||
step=".001"
|
||||
{min}
|
||||
{max}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="date"
|
||||
style={disabled ? '' : 'cursor: pointer;'}
|
||||
class="input-text"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid}
|
||||
on:click={function () {
|
||||
this.showPicker();
|
||||
}}
|
||||
style:--amount-of-buttons={isNullable ? 2.75 : 1}
|
||||
style:--button-size={isNullable ? '2rem' : '1rem'} />
|
||||
{#if isNullable}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem } from '.';
|
||||
import { createPinInput, melt } from '@melt-ui/svelte';
|
||||
|
||||
export let length: number = 6;
|
||||
export let value: string = '';
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let fullWidth = false;
|
||||
export let autoSubmit = true;
|
||||
|
||||
let element: HTMLOListElement;
|
||||
|
||||
let autoSubmitted = false;
|
||||
|
||||
const {
|
||||
elements: { root, input }
|
||||
} = createPinInput({
|
||||
placeholder: '',
|
||||
defaultValue: value.split(''),
|
||||
onValueChange: ({ next }) => {
|
||||
value = next.join('');
|
||||
|
||||
if (value.length < length) {
|
||||
autoSubmitted = false;
|
||||
}
|
||||
|
||||
if (element && autoSubmit && value.length === length && !autoSubmitted) {
|
||||
autoSubmitted = true;
|
||||
const firstInputElement = element.querySelector('input');
|
||||
firstInputElement?.form.requestSubmit();
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
const input = element.querySelector('input');
|
||||
if (element) {
|
||||
if (input && autofocus) {
|
||||
input.focus();
|
||||
}
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
<FormItem {fullWidth}>
|
||||
<ol class="u-flex u-main-center" use:melt={$root} bind:this={element}>
|
||||
{#each Array.from({ length }) as _}
|
||||
<li>
|
||||
<input
|
||||
type="number"
|
||||
class="verification-code-input u-bold u-remove-input-number-buttons"
|
||||
inputmode="numeric"
|
||||
maxlength="1"
|
||||
style:border-radius="var(--border-radius-small)"
|
||||
use:melt={$input()}
|
||||
{required}
|
||||
{readonly}
|
||||
{disabled} />
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</FormItem>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
/* Default (including mobile) */
|
||||
ol {
|
||||
gap: 0.5rem;
|
||||
|
||||
input {
|
||||
--p-input-size: 2.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* for smaller screens */
|
||||
@media #{$break2open} {
|
||||
ol {
|
||||
gap: 1rem;
|
||||
|
||||
input {
|
||||
--p-input-size: 3.75rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -13,12 +14,16 @@
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let maxlength: number = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
// https://www.geeksforgeeks.org/how-to-validate-a-domain-name-using-regular-expression/
|
||||
const pattern = String.raw`^(?!-)[A-Za-z0-9-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}$`;
|
||||
const pattern = String.raw`(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}`;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -46,9 +51,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--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>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -16,9 +17,13 @@
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let tooltip: string = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -55,9 +60,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--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>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Trim } from '$lib/components';
|
||||
import { Drop, Trim } from '$lib/components';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { Helper, Label } from '.';
|
||||
|
||||
export let label: string = null;
|
||||
@@ -13,24 +13,16 @@
|
||||
export let optionalText: string = null;
|
||||
export let tooltip: string = null;
|
||||
export let error: string = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let hovering = false;
|
||||
let show = false;
|
||||
|
||||
function setFiles(value: FileList) {
|
||||
if (!value) return;
|
||||
|
||||
const hasInvalidExt = Array.from(value).some((file) => {
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
return allowedFileExtensions?.length
|
||||
? !allowedFileExtensions.includes(fileExtension)
|
||||
: false;
|
||||
});
|
||||
if (hasInvalidExt) {
|
||||
error = 'Invalid file extension';
|
||||
return;
|
||||
}
|
||||
|
||||
files = value;
|
||||
input.files = value;
|
||||
}
|
||||
@@ -39,11 +31,22 @@
|
||||
setFiles(new DataTransfer().files);
|
||||
}
|
||||
|
||||
function isFileExtensionAllowed(fileExtension: string) {
|
||||
if (allowedFileExtensions.length && !allowedFileExtensions.includes(fileExtension)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
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];
|
||||
if (!isFileExtensionAllowed(fileExtension)) {
|
||||
error = 'Invalid file extension';
|
||||
return;
|
||||
}
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(ev.dataTransfer.items[i].getAsFile());
|
||||
@@ -78,6 +81,18 @@
|
||||
|
||||
const handleChange = (event: Event) => {
|
||||
const target = event.currentTarget as HTMLInputElement;
|
||||
|
||||
const isValidFiles = Array.from(target.files).every((file) => {
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
return isFileExtensionAllowed(fileExtension);
|
||||
});
|
||||
|
||||
if (!isValidFiles) {
|
||||
error = 'Invalid file extension';
|
||||
target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles(target.files);
|
||||
};
|
||||
</script>
|
||||
@@ -94,7 +109,30 @@
|
||||
<div>
|
||||
{#if label}
|
||||
<Label {required} {optionalText} {tooltip} hide={!label}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--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}
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import { Drop, Trim } from '$lib/components';
|
||||
import FilePicker from '$lib/components/filePicker.svelte';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Label } from '.';
|
||||
|
||||
export let label: string = null;
|
||||
export let value: Models.File = null;
|
||||
export let disabled = false;
|
||||
|
||||
export let optionalText: string = null;
|
||||
export let tooltip: string = null;
|
||||
export let isPopoverDefined = true;
|
||||
|
||||
let show = false;
|
||||
|
||||
function onSelect(file: Models.File) {
|
||||
value = file;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<Label {optionalText} {tooltip} hide={!label}>
|
||||
{label}{#if $$slots.popover && isPopoverDefined}
|
||||
<Drop bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
|
||||
<slot name="popover" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
<div
|
||||
role="region"
|
||||
class="box is-no-shadow u-padding-24 is-border-dashed"
|
||||
style:--box-border-radius="var(--border-radius-xsmall)">
|
||||
<div class="upload-file-box">
|
||||
<div class="upload-file-box-image">
|
||||
<span class="icon-upload" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-min-width-0 u-text-center">
|
||||
<h5 class="upload-file-box-title heading-level-7 u-inline">
|
||||
<span class="is-only-desktop">Select a file to upload</span>
|
||||
<span class="is-not-desktop">Select a file to upload</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="u-flex u-main-center u-cross-center u-gap-16 u-flex-vertical-mobile">
|
||||
Max file size: 1MB
|
||||
<button
|
||||
class="button is-secondary is-full-width-mobile"
|
||||
type="button"
|
||||
{disabled}
|
||||
on:click={() => (show = true)}>
|
||||
<span class="text">Browse</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if value}
|
||||
{@const fileSize = humanFileSize(value.sizeOriginal)}
|
||||
<ul class="upload-file-box-list u-min-width-0">
|
||||
<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-min-width-0">
|
||||
<Trim alternativeTrim>{value.name}</Trim>
|
||||
</span>
|
||||
<span
|
||||
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
|
||||
{fileSize.value + fileSize.unit}
|
||||
</span>
|
||||
<button
|
||||
on:click={() => (value = null)}
|
||||
type="button"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
aria-label="remove file"
|
||||
style="--button-size:1.5rem;">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if show}
|
||||
<FilePicker selectedFile={value?.$id} selectedBucket={value?.bucketId} bind:show {onSelect} />
|
||||
{/if}
|
||||
@@ -86,7 +86,7 @@
|
||||
{readonly}
|
||||
{step}
|
||||
type="number"
|
||||
class="input-text"
|
||||
class="input-text u-remove-input-number-buttons"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
@@ -15,10 +16,14 @@
|
||||
export let showPasswordButton = false;
|
||||
export let minlength = 8;
|
||||
export let maxlength: number = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let showInPlainText = false;
|
||||
let showPopover = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -46,9 +51,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show={showPopover} display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (showPopover = !showPopover)}
|
||||
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>
|
||||
|
||||
<div class="input-text-wrapper" style={showPasswordButton ? '--amount-of-buttons: 1' : ''}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -13,11 +14,15 @@
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
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}$`;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -45,9 +50,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--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>
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
|
||||
@@ -47,6 +47,24 @@
|
||||
<slot />
|
||||
{/if}
|
||||
</Label>
|
||||
<!-- <label class="choice-item" for={id}>
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{disabled}
|
||||
{required}
|
||||
{value}
|
||||
type="radio"
|
||||
bind:group
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<div
|
||||
class="choice-item-content u-cross-child-center"
|
||||
class:u-width-full-line={fullWidth}>
|
||||
<div class="choice-item-title">{label}</div>
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</label> -->
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let disabled = false;
|
||||
export let autofocus = false;
|
||||
export let isWithEndButton = true;
|
||||
export let style: string = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let element: HTMLInputElement;
|
||||
@@ -53,6 +54,7 @@
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
{style}
|
||||
type="search"
|
||||
class="input-text"
|
||||
bind:this={element}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
export let label: string | undefined = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let value: string | number | boolean;
|
||||
export let value: string | number | boolean | null;
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
export let hideRequired = false;
|
||||
export let disabled = false;
|
||||
export let wrapperTag: FormItemTag = 'li';
|
||||
export let options: {
|
||||
value: string | boolean | number;
|
||||
value: string | boolean | number | null;
|
||||
label: string;
|
||||
}[];
|
||||
export let isMultiple = false;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { DropList } from '$lib/components';
|
||||
import { SelectSearchCheckbox } from '..';
|
||||
|
||||
type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
export let name: string;
|
||||
export let tags: string[] = [];
|
||||
export let placeholder: string;
|
||||
export let options: Option[] = [];
|
||||
|
||||
let search = '';
|
||||
let input: HTMLInputElement;
|
||||
let show = false;
|
||||
|
||||
function addValue(option: Option) {
|
||||
let tag = option.value.trim();
|
||||
if (tag.length === 0 || tags.includes(tag)) return;
|
||||
|
||||
tags = [...tags, tag];
|
||||
option.checked = true;
|
||||
clearSearch();
|
||||
}
|
||||
|
||||
function removeValue(option: Option) {
|
||||
tags = tags.filter((tag) => tag !== option.value);
|
||||
option.checked = false;
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
search = '';
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
$: filteredOptions = search
|
||||
? options.filter((option) => option.label.toLowerCase().includes(search.toLowerCase()))
|
||||
: options;
|
||||
|
||||
//TODO: Debounce search and open the droplist when the user starts typing
|
||||
</script>
|
||||
|
||||
<DropList bind:show noStyle noArrow scrollable placement="bottom-end" fixed noMaxWidthList>
|
||||
<button
|
||||
class="tags-input u-position-relative u-cursor-pointer"
|
||||
type="button"
|
||||
on:click={() => (show = !show)}>
|
||||
<div class="tags">
|
||||
<ul class="tags-list">
|
||||
{#each tags as tag}
|
||||
<li class="tags-item">
|
||||
<div class="input-tag">
|
||||
<span class="tag-text">{tag}</span>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="tags-input-text u-cursor-text"
|
||||
{placeholder}
|
||||
bind:value={search}
|
||||
bind:this={input} />
|
||||
<span
|
||||
class:icon-cheveron-up={show}
|
||||
class:icon-cheveron-down={!show}
|
||||
class="u-position-absolute u-inset-block-start-4 u-inset-inline-end-12"
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<svelte:fragment slot="list">
|
||||
{#each filteredOptions as option (option.value + option.checked)}
|
||||
<SelectSearchCheckbox
|
||||
on:click={() => (option?.checked ? removeValue(option) : addValue(option))}
|
||||
bind:value={option.checked}>
|
||||
{option.label}
|
||||
</SelectSearchCheckbox>
|
||||
{:else}
|
||||
<li class="drop-list-item">
|
||||
<span class="text">There are no {name} that match your search</span>
|
||||
</li>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
@@ -8,7 +8,7 @@
|
||||
type Option = $$Generic<{
|
||||
value: string | boolean | number;
|
||||
label: string;
|
||||
data?: string[];
|
||||
data?: unknown[];
|
||||
}>;
|
||||
type OptionArray = Option[];
|
||||
|
||||
@@ -193,6 +193,17 @@
|
||||
</li>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="other">
|
||||
{#if $$slots.listEnd}
|
||||
<section class="drop-section">
|
||||
<ul class="drop-list">
|
||||
<li class="drop-list-item">
|
||||
<slot name="listEnd" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
export let label: string;
|
||||
export let id: string;
|
||||
export let value = false;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
@@ -13,10 +12,6 @@
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
@@ -31,7 +26,6 @@
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
type="checkbox"
|
||||
class="switch"
|
||||
role="switch"
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
export let readonly = false;
|
||||
export let required = false;
|
||||
export let tooltip: string = null;
|
||||
export let validityRegex: RegExp = null;
|
||||
export let validityMessage: string = null;
|
||||
|
||||
let value = '';
|
||||
let element: HTMLInputElement;
|
||||
@@ -29,11 +31,16 @@
|
||||
/**
|
||||
* Allow form submit and tab input switch
|
||||
*/
|
||||
if (value === '' && ['Enter', 'Tab'].includes(e.key)) {
|
||||
if (value === '' && ['Enter', 'Tab', ','].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
if (['Enter', 'Tab', ' '].includes(e.key)) {
|
||||
|
||||
if (['Enter', 'Tab', ' ', ','].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
if (validityRegex && !validityRegex.test(value)) {
|
||||
error = validityMessage ? validityMessage : 'Invalid value';
|
||||
return;
|
||||
}
|
||||
addValue();
|
||||
}
|
||||
if (['Backspace', 'Delete'].includes(e.key)) {
|
||||
@@ -44,6 +51,10 @@
|
||||
};
|
||||
|
||||
const addValue = () => {
|
||||
if (validityRegex && !validityRegex.test(value) && !!value) {
|
||||
error = validityMessage ? validityMessage : 'Invalid value';
|
||||
return;
|
||||
}
|
||||
let tag = value.trim();
|
||||
if (tag.length === 0 || tags.includes(tag)) return;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
import TextCounter from './textCounter.svelte';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -22,9 +23,12 @@
|
||||
export let maxlength: number = null;
|
||||
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) {
|
||||
@@ -74,7 +78,30 @@
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
{#if label}
|
||||
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style:font-size="var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
export let min: string | number | undefined = undefined;
|
||||
export let max: string | number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
function handleInvalid(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
}
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1; --button-size: 1rem">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{required}
|
||||
{min}
|
||||
{max}
|
||||
step="60"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="time"
|
||||
class="input-text"
|
||||
style={disabled ? '' : 'cursor: pointer;'}
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid}
|
||||
on:click={function () {
|
||||
this.showPicker();
|
||||
}} />
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tooltip as tooltipAction } from '$lib/actions/tooltip';
|
||||
|
||||
interface $$Props extends Partial<HTMLLabelElement> {
|
||||
required?: boolean;
|
||||
hideRequired?: boolean;
|
||||
@@ -6,6 +8,7 @@
|
||||
hide?: boolean;
|
||||
tooltip?: string;
|
||||
for?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export let required: $$Props['required'] = false;
|
||||
@@ -28,12 +31,12 @@
|
||||
{/if}
|
||||
|
||||
{#if tooltip}
|
||||
<button type="button" on:click|preventDefault class="tooltip" aria-label="input tooltip">
|
||||
<button
|
||||
type="button"
|
||||
on:click|preventDefault
|
||||
class="tooltip"
|
||||
aria-label="input tooltip"
|
||||
use:tooltipAction={{ content: tooltip }}>
|
||||
<span class="icon-info" aria-hidden="true" style="font-size: var(--icon-size-small)" />
|
||||
<span class="tooltip-popup" role="tooltip">
|
||||
<p class="text">
|
||||
{tooltip}
|
||||
</p>
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as Pill } from './pill.svelte';
|
||||
export { default as SelectSearchItem } from './selectSearchItem.svelte';
|
||||
export { default as Flag } from './flag.svelte';
|
||||
export { default as SelectSearchCheckbox } from './selectSearchCheckbox.svelte';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
// export let data: string[] = [];
|
||||
export let value = false;
|
||||
</script>
|
||||
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button u-flex u-cross-center u-gap-12"
|
||||
on:click|preventDefault
|
||||
type="button">
|
||||
<input type="checkbox" class="is-small" bind:checked={value} />
|
||||
<slot />
|
||||
</button>
|
||||
</li>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user