Merge branch 'feat-pink-v2' into 'add-date-tooltips'.

This commit is contained in:
Darshan
2025-03-17 16:08:10 +05:30
529 changed files with 17073 additions and 16654 deletions
+5 -5
View File
@@ -8,7 +8,7 @@
"build": "node build.js",
"preview": "vite preview",
"sync": "svelte-kit sync",
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning --workspace",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
@@ -19,11 +19,11 @@
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ac51fcb",
"@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@286 ",
"@appwrite.io/pink-legacy": "^1.0.2",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@286",
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bcb8ad2b",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c962928",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
+26 -26
View File
@@ -9,20 +9,20 @@ importers:
.:
dependencies:
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ac51fcb
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ac51fcb
specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464
version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
'@appwrite.io/pink-icons-svelte':
specifier: 'https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@286 '
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@286 (svelte@4.2.19)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bcb8ad2b
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bcb8ad2b(svelte@4.2.19)
'@appwrite.io/pink-legacy':
specifier: ^1.0.2
version: 1.0.2
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@286
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@286(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c962928
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c962928(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -107,7 +107,7 @@ importers:
version: 6.6.3
'@testing-library/svelte':
specifier: ^5.2.4
version: 5.2.7(svelte@4.2.19)(vite@5.4.14(@types/node@22.13.5)(sass@1.85.1))(vitest@1.6.1(@types/node@22.13.5)(@vitest/ui@1.6.1)(jsdom@22.1.0)(sass@1.85.1))
version: 5.2.7(svelte@4.2.19)(vite@5.4.14(@types/node@22.13.5)(sass@1.85.1))(vitest@1.6.1)
'@testing-library/user-event':
specifier: ^14.5.2
version: 14.6.1(@testing-library/dom@10.4.0)
@@ -211,18 +211,18 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ac51fcb':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ac51fcb}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464':
resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464}
version: 1.2.1
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@286 ':
resolution: {tarball: 'https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@286 '}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bcb8ad2b':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bcb8ad2b}
version: 1.0.0-next.7
peerDependencies:
svelte: ^4.0.0
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@85544105e5bd22ce2068c1c41e67238bad65eb82':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@85544105e5bd22ce2068c1c41e67238bad65eb82}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c962928eeee5cfc960cc1070d469e613e096b7e1':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c962928eeee5cfc960cc1070d469e613e096b7e1}
version: 1.0.0-next.7
peerDependencies:
svelte: ^4.0.0
@@ -233,11 +233,11 @@ packages:
'@appwrite.io/pink-icons@1.0.0':
resolution: {integrity: sha512-+zpksP07MvOYwhx9AZDFW0pxXQNC2juKwyOQVRAwAOkN1ACSQKPlyytkI1u2ci6CQPWjJe20CzbvBBuRNXOKjA==}
'@appwrite.io/pink-legacy@1.0.2':
resolution: {integrity: sha512-1AYNcfbV+x0Tyj56CoieSq5g7+u+7F5/LDVN/z+Hx1kp9gj7xc1eT39Dy832xxfihImF6ksjp0FXEMVSBR8cew==}
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@286':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@286}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c962928':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c962928}
version: 1.0.0-next.85
peerDependencies:
react-dom: ^18.0.0
@@ -4097,13 +4097,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@ac51fcb': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@74f9464': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@286 (svelte@4.2.19)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bcb8ad2b(svelte@4.2.19)':
dependencies:
svelte: 4.2.19
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@85544105e5bd22ce2068c1c41e67238bad65eb82(svelte@4.2.19)':
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c962928eeee5cfc960cc1070d469e613e096b7e1(svelte@4.2.19)':
dependencies:
svelte: 4.2.19
@@ -4111,14 +4111,14 @@ snapshots:
'@appwrite.io/pink-icons@1.0.0': {}
'@appwrite.io/pink-legacy@1.0.2':
'@appwrite.io/pink-legacy@1.0.3':
dependencies:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@286(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)':
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c962928(react-dom@18.3.1(react@18.3.1))(svelte@4.2.19)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@85544105e5bd22ce2068c1c41e67238bad65eb82(svelte@4.2.19)
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c962928eeee5cfc960cc1070d469e613e096b7e1(svelte@4.2.19)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.3(svelte@4.2.19))(svelte@4.2.19)
'@melt-ui/svelte': 0.86.3(svelte@4.2.19)
@@ -5375,7 +5375,7 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
'@testing-library/svelte@5.2.7(svelte@4.2.19)(vite@5.4.14(@types/node@22.13.5)(sass@1.85.1))(vitest@1.6.1(@types/node@22.13.5)(@vitest/ui@1.6.1)(jsdom@22.1.0)(sass@1.85.1))':
'@testing-library/svelte@5.2.7(svelte@4.2.19)(vite@5.4.14(@types/node@22.13.5)(sass@1.85.1))(vitest@1.6.1)':
dependencies:
'@testing-library/dom': 10.4.0
svelte: 4.2.19
-122
View File
@@ -7,128 +7,6 @@
content="Appwrite is an open-source platform for building applications at any scale, using your preferred programming languages and tools." />
<link rel="icon" type="image/svg+xml" href="/console/logos/appwrite-icon.svg" />
<link rel="mask-icon" type="image/png" href="/console/logos/appwrite-icon.png" />
<link
rel="preload"
href="/console/fonts/inter/inter-v8-latin-600.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="/console/fonts/inter/inter-v8-latin-regular.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="/console/fonts/poppins/poppins-v19-latin-500.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="/console/fonts/poppins/poppins-v19-latin-600.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="/console/fonts/poppins/poppins-v19-latin-700.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="/console/fonts/source-code-pro/source-code-pro-v20-latin-regular.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Air.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-AirItalic.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Thin.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-ThinItalic.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Light.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-LightItalic.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Regular.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-RegularItalic.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Medium.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-MediumItalic.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Bold.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-BoldItalic.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Black.woff2"
as="font"
type="font/woff2"
crossorigin />
<link
rel="preload"
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-BlackItalic.woff2"
as="font"
type="font/woff2"
crossorigin />
<link rel="preload" as="style" type="text/css" href="/console/fonts/cloud.css" />
<link rel="preload" as="style" type="text/css" href="/console/fonts/main.css" />
<link rel="stylesheet" href="/console/css/loading.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
+68 -1
View File
@@ -138,6 +138,60 @@ export function isTrackingAllowed() {
}
}
export enum Click {
BackupRestoreClick = 'click_backup_restore',
BreadcrumbClick = 'click_breadcrumb',
ConnectRepositoryClick = 'click_connect_repository',
CreditsRedeemClick = 'click_credits_redeem',
CloudSignupClick = 'click_cloud_signup',
DatabaseAttributeDelete = 'click_attribute_delete',
DatabaseIndexDelete = 'click_index_delete',
DatabaseCollectionDelete = 'click_collection_delete',
DatabaseDatabaseDelete = 'click_database_delete',
DomainCreateClick = 'click_domain_create',
DomainDeleteClick = 'click_domain_delete',
DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification',
FeedbackSubmitClick = 'click_leave_feedback',
FilterApplyClick = 'click_apply_filter',
FunctionsRedeployClick = 'click_function_redeploy',
FunctionsDeploymentDeleteClick = 'click_deployment_delete',
FunctionsDeploymentCancelClick = 'click_deployment_cancel',
KeyCreateClick = 'click_key_create',
MenuDropDownClick = 'click_menu_dropdown',
MenuOverviewClick = 'click_menu_overview',
ModalCloseClick = 'click_close_modal',
MessagingScheduleClick = 'click_messaging_schedule',
MessagingTopicCreateClick = 'click_messaging_topic_create',
MessagingTargetCreateClick = 'click_messaging_target_create',
MembershipDeleteClick = 'click_delete_membership',
PlatformCreateClick = 'click_platform_create',
OrganizationClickCreate = 'click_create_organization',
OrganizationClickUpgrade = 'click_organization_upgrade',
OnboardingSetupDatabaseClick = 'click_onboarding_setup_database',
OnboardingApiReferencesClick = 'click_onboarding_api_references',
OnboardingTutorialsClick = 'click_onboarding_tutorials',
OnboardingStorageQuickstartClick = 'click_onboarding_storage_quickstart',
OnboardingFunctionsQuickstartClick = 'click_onboarding_functions_quickstart',
OnboardingAuthEmailPasswordClick = 'click_onboarding_auth_email_password',
OnboardingAuthOauth2Click = 'click_onboarding_auth_oauth2',
OnboardingAuthAllMethodsClick = 'click_onboarding_auth_all_methods',
OnboardingDiscordClick = 'click_onboarding_discord',
StorageBucketDeleteClick = 'click_bucket_delete',
SettingsWebhookUpdateSignatureClick = 'click_webhook_update_signature',
SettingsWebhookDeleteClick = 'click_webhook_delete',
SettingsInstallProviderClick = 'click_install_provider',
SettingsStartMigrationClick = 'click_start_migration',
SubmitFormClick = 'click_submit_form',
ShowCustomIdClick = 'click_show_custom_id',
SupportOpenClick = 'click_open_support_menu',
PromoClick = 'click_promo',
PolicyDeleteClick = 'click_policy_delete',
VariablesCreateClick = 'click_variable_create',
VariablesUpdateClick = 'click_variable_update',
VariablesImportClick = 'click_variable_import',
WebsiteOpenClick = 'click_open_website'
}
export enum Submit {
DownloadDPA = 'submit_download_dpa',
Error = 'submit_error',
@@ -158,6 +212,9 @@ export enum Submit {
AccountRecoveryCodesCreate = 'submit_account_recovery_codes_create',
AccountRecoveryCodesUpdate = 'submit_account_recovery_codes_update',
AccountDeleteIdentity = 'submit_account_delete_identity',
FeedbackSubmit = 'submit_leave_feedback',
FilterClear = 'submit_clear_filter',
FilterApply = 'submit_filter_apply',
UserCreate = 'submit_user_create',
UserDelete = 'submit_user_delete',
UserUpdateEmail = 'submit_user_update_email',
@@ -165,6 +222,7 @@ export enum Submit {
UserUpdateName = 'submit_user_update_name',
UserUpdatePassword = 'submit_user_update_password',
UserUpdatePhone = 'submit_user_update_phone',
UserUpdateMfa = 'submit_user_update_mfa',
UserUpdatePreferences = 'submit_user_update_preferences',
UserUpdateStatus = 'submit_user_update_status',
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
@@ -186,9 +244,12 @@ export enum Submit {
MemberDelete = 'submit_member_delete',
MembershipUpdate = 'submit_membership_update',
MembershipUpdateStatus = 'submit_membership_update_status',
MessagingTargetUpdate = 'submit_messaging_target_update',
MessagingUpdateHtmlMode = 'submit_update_html_mode',
ProviderUpdate = 'submit_provider_update',
TeamCreate = 'submit_team_create',
TeamDelete = 'submit_team_delete',
TeamUpdatePreferences = 'submit_team_update_preferences',
TeamUpdateName = 'submit_team_update_name',
AuthLimitUpdate = 'submit_auth_limit_update',
AuthStatusUpdate = 'submit_auth_status_update',
@@ -231,6 +292,8 @@ export enum Submit {
FunctionUpdateTimeout = 'submit_function_update_timeout',
FunctionUpdateEvents = 'submit_function_update_events',
FunctionUpdateScopes = 'submit_function_key_update_scopes',
FunctionUpdateRuntime = 'submit_function_update_runtime',
FunctionUpdateBuildCommand = 'submit_function_update_build_command',
FunctionConnectRepo = 'submit_function_connect_repo',
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
FunctionRedeploy = 'submit_function_redeploy',
@@ -249,9 +312,11 @@ export enum Submit {
KeyUpdateName = 'submit_key_update_name',
KeyUpdateScopes = 'submit_key_update_scopes',
KeyUpdateExpire = 'submit_key_update_expire',
PlatformCreate = 'submit_platform_create',
PlatformDelete = 'submit_platform_delete',
PlatformUpdate = 'submit_platform_update',
DomainCreate = 'submit_domain_create',
DomainDelete = 'submit_domain_delete',
DomainUpdateVerification = 'submit_domain_update_verification',
@@ -344,5 +409,7 @@ export enum Submit {
SiteActivateDeployment = 'submit_site_activate_deployment',
RecordCreate = 'submit_dns_record_create',
RecordUpdate = 'submit_dns_record_update',
RecordDelete = 'submit_dns_record_delete'
RecordDelete = 'submit_dns_record_delete',
SearchClear = 'submit_clear_search',
FrameworkDetect = 'submit_framework_detect'
}
+5 -25
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import { Remarkable } from 'remarkable';
import Template from './template.svelte';
import { Keyboard, Layout } from '@appwrite.io/pink-svelte';
import { Alert, Keyboard, Layout } from '@appwrite.io/pink-svelte';
const markdownInstance = new Remarkable();
import { Alert, AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
import { AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
import { user } from '$lib/stores/user';
import { useCompletion } from 'ai/svelte';
import { subPanels } from '../subPanels';
@@ -218,13 +218,9 @@
{#if $error}
<div style="padding: 1rem; padding-block-end: 0;">
<Alert type="error">
<span slot="title">Something went wrong</span>
<p>
An unexpected error occurred while handling your request. Please try again
later.
</p>
</Alert>
<Alert.Inline status="error" title="Something went wrong">
An unexpected error occurred while handling your request. Please try again later.
</Alert.Inline>
</div>
{/if}
@@ -281,14 +277,6 @@
--logo-bg: #f2f2f8;
}
:global(.theme-dark) .footer {
--sep-clr: hsl(var(--color-neutral-150));
}
:global(.theme-light) .footer {
--sep-clr: hsl(var(--color-neutral-30));
}
.content {
overflow: auto;
padding: 1rem;
@@ -324,14 +312,6 @@
}
}
.footer {
.sep {
width: 1px;
height: 1.5rem;
background-color: var(--sep-clr);
}
}
.experimental {
display: flex;
padding: 0.09375rem 0.25rem;
+12 -2
View File
@@ -10,6 +10,7 @@
import { clearSubPanels, popSubPanel, subPanels } from '../subPanels';
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
import { Icon, Keyboard, Layout } from '@appwrite.io/pink-svelte';
import { Submit, trackEvent } from '$lib/actions/analytics';
/* eslint no-undef: "off" */
type Option = $$Generic<Omit<Command, 'group'> & { group?: string }>;
@@ -22,6 +23,7 @@
let selected = 0;
let usingKeyboard = false;
let contentEl: HTMLElement;
let didSearch = false;
async function triggerOption(option: Option) {
const prevPanels = $subPanels.length;
@@ -43,6 +45,14 @@
if (!open) return;
usingKeyboard = true;
if (search.length > 0) {
didSearch = true;
}
if (search === '' && didSearch) {
trackEvent(Submit.SearchClear);
}
let canceled = false;
dispatch('keydown', {
originalEvent: event,
@@ -381,8 +391,8 @@
--cmd-center-bg: var(--bgcolor-neutral-primary);
--footer-bg: var(--bgcolor-neutral-primary);
--cmd-center-border: var(--border-neutral);
--result-bg: var(--color-overlay-neutral-hover);
--kbd-bg: var(--color-overlay-on-neutral);
--result-bg: var(--overlay-neutral-hover);
--kbd-bg: var(--overlay-on-neutral);
--kbd-color: var(--fgcolor-neutral-secondary);
--icon-color: var(--fgcolor-neutral-tertiary);
--label-color: var(--fgcolor-neutral-secondary);
@@ -2,9 +2,13 @@ import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { sdk } from '$lib/stores/sdk';
import type { Searcher } from '../commands';
import { isCloud } from '$lib/system';
export const orgSearcher = (async (query: string) => {
const { teams } = await sdk.forConsole.teams.list();
const { teams } = !isCloud
? await sdk.forConsole.teams.list()
: await sdk.forConsole.billing.listOrganization();
return teams
.filter((organization) => organization.name.toLowerCase().includes(query.toLowerCase()))
.map((organization) => {
@@ -1,7 +1,7 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
@@ -26,7 +26,7 @@
<Button
href={$upgradeURL}
on:click={() => {
trackEvent('click_organization_upgrade', {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'limit_reached_banner'
});
@@ -1,7 +1,7 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
@@ -31,7 +31,7 @@
class="u-line-height-1"
href={`${base}/apply-credit?code=appw50&org=${$organization.$id}`}
on:click={() => {
trackEvent('click_credits_redeem', {
trackEvent(Click.CreditsRedeemClick, {
from: 'button',
source: 'cloud_credits_banner',
campaign: 'WelcomeManual'
+29 -37
View File
@@ -36,42 +36,34 @@
}
</script>
<FormList gap={8}>
<InputText
placeholder="Coupon code"
id="code"
label="Add credits"
{required}
hideRequired
disabled={couponData?.status === 'active'}
bind:value={coupon}>
<Button
secondary
disabled={couponData?.status === 'active' || !coupon}
on:click={addCoupon}>
Apply
</Button>
</InputText>
{#if couponData?.status === 'error'}
<InputText
placeholder="Coupon code"
id="code"
label="Add credits"
{required}
disabled={couponData?.status === 'active'}
bind:value={coupon}>
<Button secondary disabled={couponData?.status === 'active' || !coupon} on:click={addCoupon}>
Apply
</Button>
</InputText>
{#if couponData?.status === 'error'}
<div>
<span class="icon-exclamation-circle u-color-text-danger" />
<span>
{couponData.code.toUpperCase()} is not a valid promo code
</span>
</div>
{:else if couponData?.status === 'active'}
<div class="u-flex u-main-space-between u-cross-center">
<div>
<span class="icon-exclamation-circle u-color-text-danger" />
<span>
{couponData.code.toUpperCase()} is not a valid promo code
</span>
<span class="icon-tag u-color-text-success" />
<slot data={couponData}>
<span>
{couponData.code.toUpperCase()} applied (-{formatCurrency(couponData.credits)})
</span>
</slot>
</div>
{:else if couponData?.status === 'active'}
<div class="u-flex u-main-space-between u-cross-center">
<div>
<span class="icon-tag u-color-text-success" />
<slot data={couponData}>
<span>
{couponData.code.toUpperCase()} applied (-{formatCurrency(
couponData.credits
)})
</span>
</slot>
</div>
<Button icon text on:click={removeCoupon}><span class="icon-x"></span></Button>
</div>
{/if}
</FormList>
<Button icon text on:click={removeCoupon}><span class="icon-x"></span></Button>
</div>
{/if}
@@ -1,8 +1,9 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { tierToPlan, upgradeURL } from '$lib/stores/billing';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { Card } from '..';
export let service: string;
@@ -11,25 +12,24 @@
<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">
<Layout.Stack alignItems="center">
<Typography.Text variant="m-600">Upgrade to add {service}</Typography.Text>
<Typography.Text>
Upgrade to a {tierToPlan(BillingPlan.PRO).name} plan to add {service} to your organization
</p>
</Typography.Text>
<Button
class="u-margin-block-start-16"
secondary
fullWidthMobile
href={$upgradeURL}
on:click={() => {
trackEvent('click_organization_upgrade', {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: eventSource
});
}}>
Upgrade
</Button>
</div>
</Layout.Stack>
</slot>
</Card>
@@ -1,14 +1,16 @@
<script lang="ts">
import { FormList, InputChoice, InputNumber } from '$lib/elements/forms';
import { 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 type { Coupon, PlansMap } from '$lib/sdk/billing';
import { type Tier } from '$lib/stores/billing';
import { Card, Divider, Layout, Typography } from '@appwrite.io/pink-svelte';
import { CreditsApplied } from '.';
export let billingPlan: Tier;
export let collaborators: string[];
export let couponData: Partial<Coupon>;
export let plans: PlansMap;
export let billingBudget: number;
export let fixedCoupon = false; // If true, the coupon cannot be removed
export let isDowngrade = false;
@@ -18,7 +20,7 @@
let budgetEnabled = false;
$: currentPlan = $plansInfo.get(billingPlan);
$: currentPlan = plans.get(billingPlan);
$: extraSeatsCost = 0; // 0 untile trial period later replace (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
$: grossCost = currentPlan.price + extraSeatsCost;
$: estimatedTotal =
@@ -32,51 +34,44 @@
);
</script>
<section
class="card u-flex u-flex-vertical u-gap-8"
style:--p-card-padding="1.5rem"
style:--p-card-border-radius="var(--border-radius-small)">
<slot />
<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" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
<p class="text" class:u-bold={isDowngrade}>
{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">
Upcoming charge<br /><span class="u-color-text-gray"
>Due on {!currentPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</span>
</p>
<p class="text">
{formatCurrency(estimatedTotal)}
</p>
</span>
<p class="text u-margin-block-start-16">
You'll pay <span class="u-bold">{formatCurrency(estimatedTotal)}</span> now, with your first
billing cycle starting on
<span class="u-bold"
>{!currentPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</span
>. {#if couponData?.status === 'active'}Once your credits run out, you'll be charged
<span class="u-bold">{formatCurrency(currentPlan.price)}</span> plus usage fees every 30
days.
<Card.Base padding="s">
<Layout.Stack>
<slot />
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text>{currentPlan.name} plan</Typography.Text>
<Typography.Text>{formatCurrency(currentPlan.price)}</Typography.Text>
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text variant={isDowngrade ? 'm-500' : 'm-400'}
>Additional seats ({collaborators?.length})</Typography.Text>
<Typography.Text variant={isDowngrade ? 'm-500' : 'm-400'}
>{formatCurrency(extraSeatsCost)}</Typography.Text>
</Layout.Stack>
{#if couponData?.status === 'active'}
<CreditsApplied bind:couponData {fixedCoupon} />
{/if}
</p>
<Divider />
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text>
Upcoming charge<br />
Due on {!currentPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</Typography.Text>
<Typography.Text>{formatCurrency(estimatedTotal)}</Typography.Text>
</Layout.Stack>
<Typography.Text>
You'll pay <b>{formatCurrency(estimatedTotal)}</b> now, with your first billing cycle
starting on
<b
>{!currentPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</b
>. {#if couponData?.status === 'active'}Once your credits run out, you'll be charged
<b>{formatCurrency(currentPlan.price)}</b> plus usage fees every 30 days.
{/if}
</Typography.Text>
<FormList class="u-margin-block-start-24">
<InputChoice
type="switchbox"
id="budget"
@@ -87,6 +82,8 @@
{#if budgetEnabled}
<div class="u-margin-block-start-16">
<InputNumber
required
autofocus
id="budget"
label="Budget cap (USD)"
placeholder="0"
@@ -95,5 +92,5 @@
</div>
{/if}
</InputChoice>
</FormList>
</section>
</Layout.Stack>
</Card.Base>
+43 -59
View File
@@ -1,11 +1,12 @@
<script lang="ts">
import { FormList, InputChoice, InputText } from '$lib/elements/forms';
import { onDestroy, onMount } from 'svelte';
import { CreditCardBrandImage, RadioBoxes } from '..';
import { unmountPaymentElement } from '$lib/stores/stripe';
import { Pill } from '$lib/elements';
import { InputChoice, InputText } from '$lib/elements/forms';
import { onMount } from 'svelte';
import { CreditCardBrandImage } from '..';
import { initializeStripe, unmountPaymentElement } from '$lib/stores/stripe';
import { Badge, Card, Layout } from '@appwrite.io/pink-svelte';
import type { PaymentMethodData } from '$lib/sdk/billing';
export let methods: Record<string, unknown>[];
export let methods: PaymentMethodData[];
export let group: string;
export let name: string;
export let defaultMethod: string = null;
@@ -36,61 +37,48 @@
}
}
});
});
onDestroy(() => {
observer.disconnect();
unmountPaymentElement();
return () => {
observer.disconnect();
unmountPaymentElement();
};
});
$: if (element) {
initializeStripe(element);
observer.observe(element, { childList: true });
}
//Set setAsDefault as false when group changes
$: if (group || group === null) {
$: if (group || group === '$new') {
setAsDefault = false;
}
</script>
<RadioBoxes
elements={methods}
total={methods?.length}
variableName="$id"
name="payment"
bind:group
{disabledCondition}>
<svelte:fragment slot="element" let:element>
<slot {element}>
<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>
<span class="u-capitalize">{element.brand}</span> ending in {element.last4}</span>
<CreditCardBrandImage brand={element.brand?.toString()} />
</span>
{#if element.$id === backupMethod}
<Pill>Backup</Pill>
{:else if element.$id === defaultMethod}
<Pill>Default</Pill>
{/if}
</span>
{#if !!defaultMethod && element.$id !== defaultMethod && group === element.$id && showSetAsDefault && element.$id !== backupMethod}
<ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
</ul>
<Layout.Stack>
{#each methods as method}
{@const value = method.$id}
<Card.Selector
title={method.name}
name={value}
bind:group
{value}
disabled={disabledCondition ? value === disabledCondition : false}>
<svelte:fragment slot="action">
{#if method.$id === backupMethod}
<Badge variant="secondary" content="Backup" size="xs" />
{:else if method.$id === defaultMethod}
<Badge variant="secondary" content="Default" size="xs" />
{/if}
</span>
</slot>
</svelte:fragment>
<svelte:fragment slot="new">
<span style="padding-inline:0.25rem">Add new payment method</span>
</svelte:fragment>
<FormList class="u-margin-block-start-8" gap={16}>
</svelte:fragment>
<Layout.Stack direction="row" alignItems="center" gap="s">
{method.brand} ending in {method.last4}
<CreditCardBrandImage brand={method.brand?.toString()} />
</Layout.Stack>
</Card.Selector>
{/each}
<Card.Selector title="Add new payment method" name="$new" bind:group value="$new" />
{#if group === '$new'}
<InputText
id="name"
label="Cardholder name"
@@ -103,20 +91,16 @@
<div class="loader-container" bind:this={loader}>
<div class="loader"></div>
</div>
<div id="payment-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
<div bind:this={element}></div>
</div>
{#if showSetAsDefault}
<ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
</ul>
<InputChoice
bind:value={setAsDefault}
id="default"
label="Set as default payment method for this organization" />
{/if}
</FormList>
</RadioBoxes>
{/if}
</Layout.Stack>
<style lang="scss">
.aw-stripe-container {
+31 -38
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import { FakeModal } from '$lib/components';
import { InputText, Button, FormList } from '$lib/elements/forms';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { initializeStripe, submitStripeCard } from '$lib/stores/stripe';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { page } from '$app/stores';
import { Layout, Spinner } from '@appwrite.io/pink-svelte';
export let show = false;
@@ -15,10 +16,6 @@
let name: string;
let error: string;
onMount(async () => {
await initializeStripe();
});
async function handleSubmit() {
try {
const card = await submitStripeCard(name, $page?.params?.organization ?? null);
@@ -34,12 +31,13 @@
}
}
let isLoading = true;
let element: HTMLElement;
let loader: HTMLDivElement;
let observer: MutationObserver;
onMount(() => {
initializeStripe(element);
observer = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
@@ -49,18 +47,17 @@
node instanceof Element &&
node.className.toLowerCase().includes('__privatestripeelement')
) {
loader.style.display = 'none';
isLoading = false;
}
}
}
}
}
});
});
onDestroy(() => {
observer.disconnect();
document.documentElement.classList.remove('u-overflow-hidden');
return () => {
observer.disconnect();
};
});
$: if (element) {
@@ -69,26 +66,25 @@
</script>
<FakeModal bind:show title="Add payment method" bind:error onSubmit={handleSubmit}>
<FormList gap={16}>
<slot />
<InputText
id="name"
label="Cardholder name"
placeholder="Cardholder name"
bind:value={name}
required
autofocus={true}
hideRequired />
<div class="aw-stripe-container" data-private>
<div class="loader-container" bind:this={loader}>
<div class="loader"></div>
</div>
<div id="payment-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
<slot />
<InputText
id="name"
required
autofocus={true}
bind:value={name}
label="Cardholder name"
placeholder="Cardholder name" />
<div class="aw-stripe-container" data-private>
{#if isLoading}
<Spinner />
{/if}
<div class="stripe-element" bind:this={element}>
<!-- Stripe will create form elements here -->
</div>
<slot name="end"></slot>
</FormList>
</div>
<slot name="end"></slot>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Cancel</Button>
<Button submit disabled={!name}>Add</Button>
@@ -97,14 +93,11 @@
<style lang="scss">
.aw-stripe-container {
min-height: 295px;
position: relative;
.loader-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
display: flex;
min-height: 245px;
.stripe-element {
width: 100%;
}
}
</style>
@@ -2,38 +2,40 @@
import { BillingPlan } from '$lib/constants';
import { formatNum } from '$lib/helpers/string';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
import { Card, Layout, Tabs, Typography } from '@appwrite.io/pink-svelte';
export let downgrade = false;
let selectedTab: Tier = BillingPlan.FREE;
export let downgrade = false;
$: plan = $plansInfo.get(selectedTab);
</script>
<Card style="--card-padding: 1.5rem">
<div class="comparison-box">
<SecondaryTabs stretch>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.FREE}
<Card.Base>
<Layout.Stack>
<Tabs.Root stretch let:root>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.FREE}
on:click={() => (selectedTab = BillingPlan.FREE)}>
{tierFree.name}
</SecondaryTabsItem>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.PRO}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.PRO}
on:click={() => (selectedTab = BillingPlan.PRO)}>
{tierPro.name}
</SecondaryTabsItem>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.SCALE}
</Tabs.Item.Button>
<Tabs.Item.Button
{root}
active={selectedTab === BillingPlan.SCALE}
on:click={() => (selectedTab = BillingPlan.SCALE)}>
{tierScale.name}
</SecondaryTabsItem>
</SecondaryTabs>
</div>
</Tabs.Item.Button>
</Tabs.Root>
<div class="u-margin-block-start-24">
<Typography.Text variant="m-600">{plan.name} plan</Typography.Text>
{#if selectedTab === BillingPlan.FREE}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
{#if downgrade}
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
<li class="list-item u-gap-4 u-cross-center">
@@ -67,37 +69,26 @@
</li>
</ul>
{:else}
<ul class="u-margin-block-start-8 un-order-list">
<ul class="un-order-list">
<li>
<span class="text">
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
Functions per project
</span>
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
Functions per project
</li>
<li>Limited to 1 organization member</li>
<li>
Limited to {plan.bandwidth}GB bandwidth
</li>
<li>
<span class="text"> Limited to 1 organization member </span>
Limited to {plan.storage}GB storage
</li>
<li>
<span class="text">
{plan.bandwidth}GB bandwidth
</span>
</li>
<li>
<span class="text">
{plan.storage}GB storage
</span>
</li>
<li>
<span class="text">
{formatNum(plan.executions)} executions
</span>
Limited to {formatNum(plan.executions)} executions
</li>
</ul>
{/if}
{:else if selectedTab === BillingPlan.PRO}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
<p class="u-margin-block-start-8">Everything in the Free plan, plus:</p>
<ul class="un-order-list u-margin-inline-start-4">
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
<ul class="un-order-list">
<li>Unlimited databases, buckets, functions</li>
<li>{plan.bandwidth}GB bandwidth</li>
<li>{plan.storage}GB storage</li>
@@ -105,9 +96,8 @@
<li>Email support</li>
</ul>
{:else if selectedTab === BillingPlan.SCALE}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
<ul class="un-order-list u-margin-inline-start-4">
<Typography.Text>Everything in the Pro plan, plus:</Typography.Text>
<ul class="un-order-list">
<li>Unlimited seats</li>
<li>Organization roles</li>
<li>SOC-2, HIPAA compliance</li>
@@ -115,29 +105,5 @@
<li>Priority support</li>
</ul>
{/if}
</div>
</Card>
<style lang="scss">
.comparison-box {
border-radius: var(--border-radius-small);
background: hsl(var(--color-neutral-5));
}
:global(.theme-dark) .comparison-box {
background: hsl(var(--color-neutral-85));
}
.comparison-box :global(.secondary-tabs-button:where(:disabled)) {
background: hsl(var(--color-neutral-0));
border: 1px solid hsl(var(--color-neutral-10));
}
:global(.theme-dark) .comparison-box :global(.secondary-tabs-button:where(:disabled)) {
background: hsl(var(--color-neutral-80));
border: 1px solid hsl(var(--color-neutral-85));
}
.inline-tag {
line-height: 140%;
font-weight: 500;
}
</style>
</Layout.Stack>
</Card.Base>
+78 -89
View File
@@ -1,14 +1,4 @@
<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';
@@ -20,7 +10,9 @@
import type { OrganizationUsage } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { BillingPlan } from '$lib/constants';
import { Tooltip } from '@appwrite.io/pink-svelte';
import { Alert, Icon, Table, Tooltip } from '@appwrite.io/pink-svelte';
import Cell from '$lib/elements/table/cell.svelte';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
export let tier: Tier;
export let members: number;
@@ -48,12 +40,11 @@
</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>
<Alert.Inline
status="error"
title={`Your ${tierToPlan($organization.billingPlan).name} plan subscription will end on ${toLocaleDate(
$organization.billingNextInvoiceDate
)}`}>
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
@@ -67,80 +58,78 @@
Learn more
</Button>
</svelte:fragment>
</Alert>
</Alert.Inline>
<TableScroll noMargin dense class="u-margin-block-start-16">
<TableHeader>
<TableCellHead>Resource</TableCellHead>
<TableCellHead>Free limit</TableCellHead>
<TableCellHead>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Resource</Table.Header.Cell>
<Table.Header.Cell>Free limit</Table.Header.Cell>
<Table.Header.Cell>
Excess usage <Tooltip
><span class="icon-info"></span>
><Icon icon={IconInfo} />
<span slot="tooltip">Metrics are estimates updated every 24 hours</span>
</Tooltip>
</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).value}
{humanFileSize(excess?.storage).unit}
</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>
</Table.Header.Cell>
</svelte:fragment>
{#if excess?.members}
<Table.Row>
<Table.Cell>Organization members</Table.Cell>
<Table.Cell>{plan.members} members</Table.Cell>
<Table.Cell>
<p class="u-color-text-danger u-flex u-cross-center u-gap-4">
<span class="icon-arrow-up" />
{excess?.members} members
</p>
</Table.Cell>
</Table.Row>
{/if}
{#if excess?.storage}
<Table.Row>
<Table.Cell>Storage</Table.Cell>
<Table.Cell>{plan.storage} GB</Table.Cell>
<Table.Cell>
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
{humanFileSize(excess?.storage).value}
{humanFileSize(excess?.storage).unit}
</p>
</Table.Cell>
</Table.Row>
{/if}
{#if excess?.executions}
<Table.Row>
<Table.Cell>Function executions</Table.Cell>
<Table.Cell>
{abbreviateNumber(plan.executions)} executions
</Table.Cell>
<Table.Cell>
<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>
</Table.Cell>
</Table.Row>
{/if}
{#if excess?.users}
<Table.Row>
<Table.Cell>Users</Table.Cell>
<Table.Cell>
{abbreviateNumber(plan.users)} users
</Table.Cell>
<Table.Cell>
<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>
</Table.Cell>
</Table.Row>
{/if}
</Table.Root>
{/if}
+58 -78
View File
@@ -3,6 +3,7 @@
import { formatCurrency } from '$lib/helpers/numbers';
import { plansInfo, type Tier, tierFree, tierPro, tierScale } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
import { LabelCard } from '..';
export let billingPlan: Tier;
@@ -17,81 +18,60 @@
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
</script>
{#if billingPlan}
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={anyOrgFree || !selfService}
value={BillingPlan.FREE}
tooltipShow={anyOrgFree}
title={tierFree.name}
tooltipText="You are limited to 1 Free organization per account."
padding="m">
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={anyOrgFree || !selfService}>
<h4 class="body-text-2 u-bold">
{tierFree.name}
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierFree.description}
</p>
<p>
{formatCurrency(freePlan?.price ?? 0)}
</p>
</div>
</LabelCard>
</li>
<li>
<LabelCard
name="plan"
disabled={!selfService}
bind:group={billingPlan}
value={BillingPlan.PRO}
title={tierPro.name}
padding="m">
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={!selfService}>
<h4 class="body-text-2 u-bold">
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierPro.description}
</p>
<p>
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
</p>
</div>
</LabelCard>
</li>
<li>
<LabelCard name="plan" bind:group={billingPlan} value={BillingPlan.SCALE} padding={1.5}>
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{tierScale.name}
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierScale.description}
</p>
<p>
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
</ul>
{/if}
<Layout.Stack>
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={anyOrgFree || !selfService}
value={BillingPlan.FREE}
tooltipShow={anyOrgFree}
title={tierFree.name}
tooltipText="You are limited to 1 Free organization per account.">
<svelte:fragment slot="action">
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Typography.Caption variant="400">
{tierFree.description}
</Typography.Caption>
<Typography.Text>
{formatCurrency(freePlan?.price ?? 0)}
</Typography.Text>
</LabelCard>
<LabelCard
name="plan"
disabled={!selfService}
bind:group={billingPlan}
value={BillingPlan.PRO}
title={tierPro.name}>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Typography.Caption variant="400">
{tierPro.description}
</Typography.Caption>
<Typography.Text>
{formatCurrency(proPlan?.price ?? 0)} per month + usage
</Typography.Text>
</LabelCard>
<LabelCard
name="plan"
bind:group={billingPlan}
value={BillingPlan.SCALE}
title={tierScale.name}>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Typography.Caption variant="400">
{tierScale.description}
</Typography.Caption>
<Typography.Text>
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</Typography.Text>
</LabelCard>
</Layout.Stack>
@@ -1,14 +1,15 @@
<script lang="ts">
import { Button, Helper, InputChoice, InputSelectSearch, InputText } from '$lib/elements/forms';
import { Button, InputChoice, 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';
import { Icon } from '@appwrite.io/pink-svelte';
import { Alert, Fieldset, Icon, Layout, Selector } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
export let methods: PaymentList;
export let value: string;
@@ -16,22 +17,10 @@
let showTaxId = false;
let showPaymentModal = false;
let input: HTMLInputElement;
let error: string;
function handleInvalid(event: Event) {
event.preventDefault();
if (input.validity.valueMissing) {
error = 'This field is required';
return;
}
error = input.validationMessage;
}
async function cardSaved(event: CustomEvent<PaymentMethodData>) {
value = event.detail.$id;
methods = await sdk.forConsole.billing.listPaymentMethods();
invalidate(Dependencies.UPGRADE_PLAN);
}
onMount(() => {
@@ -41,114 +30,67 @@
});
$: filteredMethods = methods?.paymentMethods?.filter((method) => !!method?.last4);
$: selectedPaymentMethod = methods?.paymentMethods?.find((method) => method.$id === value);
</script>
{#if filteredMethods?.length}
{#if selectedPaymentMethod?.country?.toLowerCase() === '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)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add new payment method
</Button>
</svelte:fragment>
</InputSelectSearch>
{:else}
<div>
<input
bind:this={input}
on:invalid={handleInvalid}
required
class="u-hide"
type="text"
name="method"
id="method" />
<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>
<span class="icon-exclamation-circle"></span>
<span class="text">No saved payment methods</span>
</p>
<Fieldset legend="Payment">
<Layout.Stack>
{#if filteredMethods?.length}
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
<Alert.Inline status="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.Inline>
{/if}
<InputSelect
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]
};
})} />
<div>
<Button secondary on:click={() => (showPaymentModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add new payment method
</Button>
</div>
{:else}
<Alert.Inline title="No saved payment methods">
<Button slot="actions" secondary on:click={() => (showPaymentModal = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add
</Button>
</div>
</Card>
{#if error}
<Helper class="u-position-relative" type="warning">{error}</Helper>
</Alert.Inline>
{/if}
</div>
{/if}
</Layout.Stack>
</Fieldset>
{#if showPaymentModal && isCloud && hasStripePublicKey}
<PaymentModal bind:show={showPaymentModal} on:submit={cardSaved}>
<svelte:fragment slot="end">
<InputChoice
type="checkbox"
<Selector.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"
autofocus
placeholder="Tax ID"
bind:value={taxId} />
</div>
{/if}
</InputChoice>
bind:checked={showTaxId} />
{#if showTaxId}
<InputText
id="taxId"
label="Tax ID"
autofocus
placeholder="Tax ID"
bind:value={taxId} />
{/if}
</svelte:fragment>
</PaymentModal>
{/if}
+49 -58
View File
@@ -1,19 +1,12 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import {
Table,
TableBody,
TableCellHead,
TableCellText,
TableHeader,
TableRow
} from '$lib/elements/table';
import { toLocaleDate } from '$lib/helpers/date';
import { organization, type Organization } from '$lib/stores/organization';
import { type Organization } from '$lib/stores/organization';
import { plansInfo } from '$lib/stores/billing';
import { abbreviateNumber, formatCurrency } from '$lib/helpers/numbers';
import { BillingPlan } from '$lib/constants';
import { Table, Typography } from '@appwrite.io/pink-svelte';
export let show = false;
export let org: Organization;
@@ -22,7 +15,7 @@
$: nextDate = org?.name
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
: $organization?.billingNextInvoiceDate;
: org?.billingNextInvoiceDate;
const planData = [
{
@@ -52,64 +45,62 @@
$: isFree = org.billingPlan === BillingPlan.FREE;
</script>
<Modal bind:show size="big" title="Usage rates">
<Modal bind:show title="Usage rates">
{#if isFree}
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
Next billing period: {toLocaleDate(nextDate)}.
<Typography.Text>
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
Next billing period: {toLocaleDate(nextDate)}.
</Typography.Text>
{:else if org.billingPlan === BillingPlan.PRO}
<p>
<Typography.Text>
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>
</Typography.Text>
{:else if org.billingPlan === BillingPlan.SCALE}
<p>
<Typography.Text>
Usage on the Scale plan will be charged at the end of each billing period at the
following rates. Next billing period: {toLocaleDate(nextDate)}.
</p>
</Typography.Text>
{/if}
<Table noStyles noMargin>
<TableHeader>
<TableCellHead>Resource</TableCellHead>
<TableCellHead>Limit</TableCellHead>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Resource</Table.Header.Cell>
<Table.Header.Cell>Limit</Table.Header.Cell>
{#if !isFree}
<TableCellHead>Rate</TableCellHead>
<Table.Header.Cell>Rate</Table.Header.Cell>
{/if}
</TableHeader>
<TableBody>
{#each planData as usage}
{#if usage['id'] === 'members'}
<TableRow>
<TableCellText title="resource">{usage.resource}</TableCellText>
<TableCellText title="limit">
{plan[usage.id] || 'Unlimited'}
</TableCellText>
{#if !isFree}
<TableCellText title="rate">
{formatCurrency(plan.addons.member.price)}/{usage?.unit}
</TableCellText>
{/if}
</TableRow>
{:else}
{@const addon = plan.addons[usage.id]}
<TableRow>
<TableCellText title="resource">{usage.resource}</TableCellText>
<TableCellText title="limit">
{abbreviateNumber(plan[usage.id])}{usage?.unit}
</TableCellText>
{#if !isFree}
<TableCellText title="rate">
{formatCurrency(addon?.price)}/{['MB', 'GB', 'TB'].includes(
addon?.unit
)
? addon?.value
: abbreviateNumber(addon?.value, 0)}{usage?.unit}
</TableCellText>
{/if}
</TableRow>
{/if}
{/each}
</TableBody>
</Table>
</svelte:fragment>
{#each planData as usage}
{#if usage['id'] === 'members'}
<Table.Row>
<Table.Cell>{usage.resource}</Table.Cell>
<Table.Cell>
{plan[usage.id] || 'Unlimited'}
</Table.Cell>
{#if !isFree}
<Table.Cell>
{formatCurrency(plan.addons?.member?.price)}/{usage?.unit}
</Table.Cell>
{/if}
</Table.Row>
{:else}
{@const addon = plan.addons[usage.id]}
<Table.Row>
<Table.Cell>{usage.resource}</Table.Cell>
<Table.Cell>
{abbreviateNumber(plan[usage.id])}{usage?.unit}
</Table.Cell>
{#if !isFree}
<Table.Cell>
{formatCurrency(addon?.price)}/{['MB', 'GB', 'TB'].includes(addon?.unit)
? addon?.value
: abbreviateNumber(addon?.value, 0)}{usage?.unit}
</Table.Cell>
{/if}
</Table.Row>
{/if}
{/each}
</Table.Root>
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Close</Button>
</svelte:fragment>
@@ -1,20 +1,20 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button, FormList, InputText } from '$lib/elements/forms';
import { Button, 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
};
let error: string = null;
let coupon: string = '';
const dispatch = createEventDispatcher();
async function addCoupon() {
try {
@@ -37,15 +37,20 @@
}
</script>
<Modal bind:show title="Add credits" onSubmit={addCoupon} size="big" bind:error>
Credits will be applied automatically to your next invoice.
<Modal bind:show title="Add credits" onSubmit={addCoupon} bind:error>
<svelte:fragment slot="description">
Credits will be applied automatically to your next invoice.
</svelte:fragment>
<FormList>
<InputText placeholder="Promo code" id="code" label="Add promo code" bind:value={coupon} />
</FormList>
<InputText
required
placeholder="Promo code"
id="code"
label="Add promo code"
bind:value={coupon} />
<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button submit>Add</Button>
<Button submit disabled={coupon === ''}>Add</Button>
</svelte:fragment>
</Modal>
@@ -1,13 +1,14 @@
<script lang="ts">
import type { SheetMenu, SubMenu } from '$lib/components/bottom-sheet/index.js';
import { ActionMenu } from '@appwrite.io/pink-svelte';
import { ActionMenu, Layout, Selector } from '@appwrite.io/pink-svelte';
export let menu: SubMenu;
export let isOpen: boolean;
export let navigateSubMenu: (menu: SheetMenu) => void;
export let navigatePreviousMenu: () => void;
</script>
{#if menu.title}
{#if menu?.title}
<span class="menu-title">{menu.title}</span>
{/if}
<ActionMenu.Root>
@@ -31,12 +32,23 @@
on:click={() => {
if (menuItem.subMenu) {
navigateSubMenu(menuItem.subMenu);
} else if (menuItem.navigatePrevious) {
navigatePreviousMenu();
} else if (menuItem.onClick !== undefined) {
menuItem.onClick();
isOpen = false;
if (menuItem.closeOnClick !== false) {
isOpen = false;
}
}
}}>
{menuItem.name}
{#if menuItem?.checked !== undefined}
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox checked={menuItem.checked} size="s" />
{menuItem.name}
</Layout.Stack>
{:else}
{menuItem.name}
{/if}
</ActionMenu.Item.Button>
{/if}
{/each}
@@ -8,8 +8,10 @@
let sheetContainerRef: $$Props['sheetContainerRef'];
let activeMenu = menu;
let showDivider = true;
let previousMenu = activeMenu;
function navigateSubMenu(subMenu: SheetMenu) {
previousMenu = activeMenu;
if (sheetContainerRef) {
const currentHeight = sheetContainerRef.offsetHeight;
sheetContainerRef.style.overflowY = 'hidden';
@@ -27,6 +29,11 @@
showDivider = activeMenu.bottom !== undefined;
}
function navigatePreviousMenu() {
activeMenu = previousMenu;
showDivider = activeMenu.bottom !== undefined;
}
function restoreMenu(isOpenState: boolean) {
showDivider = activeMenu.bottom !== undefined;
if (!isOpenState) {
@@ -40,11 +47,20 @@
</script>
<BottomSheet.Default bind:isOpen useSlots={true} bind:sheetContainerRef bind:showDivider>
<div slot="top"><SheetMenuBlock menu={activeMenu.top} {navigateSubMenu} bind:isOpen /></div>
<div slot="top">
<SheetMenuBlock
menu={activeMenu.top}
{navigateSubMenu}
{navigatePreviousMenu}
bind:isOpen />
</div>
<div slot="bottom">
{#if activeMenu.bottom}<SheetMenuBlock
{#if activeMenu.bottom}
<SheetMenuBlock
menu={activeMenu.bottom}
{navigateSubMenu}
bind:isOpen />{/if}
{navigatePreviousMenu}
bind:isOpen />
{/if}
</div>
</BottomSheet.Default>
+3
View File
@@ -19,6 +19,9 @@ type MenuItem = {
trailingIcon?: ComponentType;
onClick?: () => void;
href?: string;
closeOnClick?: boolean;
navigatePrevious?: boolean;
checked?: boolean;
subMenu?: { top: SubMenu; bottom: SubMenu };
};
+3 -3
View File
@@ -15,7 +15,7 @@
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
import { project } from '$routes/(console)/project-[project]/store';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
let currentIndex = 0;
let openModalOnMobile = false;
@@ -170,7 +170,7 @@
external={!!currentModalAlert.cta.external}
fullWidthMobile
on:click={() => {
trackEvent('click_promo', {
trackEvent(Click.PromoClick, {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
});
@@ -282,7 +282,7 @@
fullWidthMobile
on:click={() => {
openModalOnMobile = false;
trackEvent('click_promo', {
trackEvent(Click.PromoClick, {
promo: currentModalAlert.id,
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
});
+7 -5
View File
@@ -13,6 +13,7 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { newOrgModal } from '$lib/stores/organization';
import { Click, trackEvent } from '$lib/actions/analytics';
type Project = {
name: string;
@@ -68,6 +69,7 @@
let projectsBottomSheetOpen = false;
function createOrg() {
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
if (isCloud) {
goto(`${base}/create-organization`);
} else newOrgModal.set(true);
@@ -397,7 +399,7 @@
:global(.item[data-highlighted]) {
border-radius: var(--border-radius-S, 8px);
background: var(--color-overlay-neutral-hover, rgba(25, 25, 28, 0.03));
background: var(--overlay-neutral-hover, rgba(25, 25, 28, 0.03));
}
.trigger {
display: inline-flex;
@@ -412,7 +414,7 @@
color: var(--fgcolor-neutral-primary, #2d2d31);
border-radius: var(--corner-radius-medium, 8px);
cursor: default;
cursor: pointer;
/* Body text/level 2 Regular */
font-family: Inter;
font-size: 14px;
@@ -422,7 +424,7 @@
}
.trigger:hover {
background: var(--color-overlay-neutral-hover, rgba(25, 25, 28, 0.03));
background: var(--overlay-neutral-hover, rgba(25, 25, 28, 0.03));
}
:global(.trigger[data-highlighted]) {
@@ -430,12 +432,12 @@
background: var(--bgcolor-neutral-secondary, #f4f4f7);
}
:global(.trigger[data-highlighted]:focus) {
:global(.trigger[data-highlighted]:focus-visible) {
outline: none;
box-shadow: 0 0 0 2px var(--bgcolor-neutral-secondary, #f4f4f7);
}
.trigger:focus {
.trigger:focus-visible {
z-index: 30;
box-shadow:
var(--shadow-offsetx-0, 0px) var(--shadow-offsety-0, 0px) 0 2px
+11 -4
View File
@@ -19,10 +19,7 @@
$: limit = preferences.get($page.route)?.limit ?? CARD_LIMIT;
</script>
<ul
class="grid-box"
style={`--grid-gap:1.5rem; --grid-item-size-small-screens: 18rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
data-private>
<ul class="grid-box" style={`--grid-item-size:${total > 3 ? '22rem' : '25rem'};`} data-private>
<slot />
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
@@ -35,3 +32,13 @@
{/if}
{/if}
</ul>
<style lang="scss">
.grid-box {
display: grid;
grid-auto-rows: 1fr;
gap: var(--gap-xl);
flex-shrink: 0;
grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-size), 1fr));
}
</style>
+8 -14
View File
@@ -3,23 +3,24 @@
export let hideOverflow = false;
export let hideFooter = false;
export let gap: 'none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl' = 'l';
</script>
<Card.Base>
<Layout.Stack gap="xl" justifyContent="space-around">
<div class="common-section grid-1-2" class:hideOverflow>
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-4">
<Typography.Title size="s"><slot name="title" /></Typography.Title>
<Layout.GridFraction gap="xxxl" rowGap="xl" start={1} end={2}>
<Layout.Stack gap="xxs">
<Typography.Title size="s" truncate><slot name="title" /></Typography.Title>
{#if $$slots.default}
<Typography.Text>
<slot />
</Typography.Text>
{/if}
</div>
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
</Layout.Stack>
<Layout.Stack {gap}>
<slot name="aside" />
</div>
</div>
</Layout.Stack>
</Layout.GridFraction>
{#if $$slots.actions && !hideFooter}
<span
style="margin-left: calc(-1* var(--space-9));margin-right: calc(-1* var(--space-9));width:auto;">
@@ -31,10 +32,3 @@
{/if}
</Layout.Stack>
</Card.Base>
<style lang="scss">
.hideOverflow > * {
width: 100%;
overflow: hidden;
}
</style>
+7 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { upgradeURL } from '$lib/stores/billing';
import { Click, trackEvent } from '$lib/actions/analytics';
export let service: string;
</script>
@@ -8,6 +9,11 @@
<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 href={$upgradeURL}>Change plan</Button>
<Button
secondary
href={$upgradeURL}
on:click={() => {
trackEvent(Click.OrganizationClickUpgrade, { source: 'card_plan_limit' });
}}>Change plan</Button>
</div>
</article>
+30 -33
View File
@@ -1,41 +1,38 @@
<script lang="ts">
import type { PaymentMethodData } from '$lib/sdk/billing';
import { Alert } from '.';
import { Badge, Layout, Link, Popover, Table } from '@appwrite.io/pink-svelte';
import CreditCardBrandImage from './creditCardBrandImage.svelte';
export let isBox = false;
export let paymentMethod: PaymentMethodData;
export let isBackup: boolean = false;
</script>
<div class:box={isBox}>
<div class="u-flex u-main-space-between u-cross-start" style="padding-block: 0.5rem;">
<div class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2">
<span class="u-flex u-cross-center u-gap-8">
<p class="text u-bold">
<span class="u-capitalize">{paymentMethod?.brand}</span> ending in {paymentMethod?.last4}
</p>
<CreditCardBrandImage brand={paymentMethod?.brand} />
</span>
<p class="text">
Expires {paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}
</p>
{#if paymentMethod?.name}
<p class="text">
{paymentMethod.name}
</p>
{/if}
</div>
<slot />
</div>
{#if paymentMethod?.expired}
<Alert type="error" class="u-margin-block-start-16 u-width-full-line">
<svelte:fragment slot="title">This payment method has expired</svelte:fragment>
</Alert>
<Table.Cell>
<Layout.Stack direction="row" alignItems="center" gap="s">
<CreditCardBrandImage brand={paymentMethod?.brand} />
<span>ending in {paymentMethod?.last4}</span>
{#if isBackup}
<Badge variant="secondary" content="Backup" />
{/if}
</Layout.Stack>
</Table.Cell>
<Table.Cell>{paymentMethod?.name}</Table.Cell>
<Table.Cell>{paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}</Table.Cell>
<Table.Cell>
{#if paymentMethod?.lastError || paymentMethod?.expired}
<Popover let:toggle>
<Layout.Stack gap="xs" direction="row">
<Badge variant="secondary" type="error" content="Failed" />
<Link.Button on:click={toggle}>Details</Link.Button>
</Layout.Stack>
<svelte:fragment slot="tooltip">
{#if paymentMethod?.expired}
This payment method has expired
{/if}
{#if paymentMethod?.lastError}
{paymentMethod.lastError}
{/if}
</svelte:fragment>
</Popover>
{/if}
{#if paymentMethod?.lastError}
<Alert type="error" class="u-margin-block-start-16 u-width-full-line">
{paymentMethod.lastError}
</Alert>
{/if}
</div>
</Table.Cell>
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { InputId } from '$lib/elements/forms';
import { InputProjectId } from '$lib/elements/forms';
import Button from '$lib/elements/forms/button.svelte';
@@ -20,7 +20,7 @@
}
$: if (show) {
trackEvent('click_show_custom_id');
trackEvent(Click.ShowCustomIdClick);
}
</script>
+23 -17
View File
@@ -1,26 +1,32 @@
<script lang="ts">
import PaginationInline from './paginationInline.svelte';
import { Card, Empty } from '@appwrite.io/pink-svelte';
import { Card, Empty, Layout } from '@appwrite.io/pink-svelte';
export let hidePagination = false;
export let hidePages = false;
export let target = '';
export let search = '';
</script>
<Card.Base padding="none">
<Empty
title={`Sorry, we couldn't find ${search ? `${search}` : `any ${target}`}`}
description={`There are no ${target} that match your search.`}
type="secondary">
<svelte:fragment slot="actions">
<slot />
</svelte:fragment>
</Empty>
</Card.Base>
<Layout.Stack gap="l">
<Card.Base padding="none">
<Empty
title={`Sorry, we couldn't find ${search ? `${search}` : `any ${target}`}`}
description={`There are no ${target} that match your search.`}
type="secondary">
<svelte:fragment slot="actions">
<slot />
</svelte:fragment>
</Empty>
</Card.Base>
{#if !hidePagination}
<div class="u-flex u-margin-block-start-32 u-main-space-between u-cross-center">
<p class="text">Total results: 0</p>
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
</div>
{/if}
{#if !hidePagination}
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
wrap="wrap">
<p class="text">Total results: 0</p>
<PaginationInline limit={1} offset={0} sum={0} {hidePages} />
</Layout.Stack>
{/if}
</Layout.Stack>
+8 -4
View File
@@ -2,7 +2,7 @@
import { Alert } from '$lib/components';
import { onMount } from 'svelte';
import Form from '$lib/elements/forms/form.svelte';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { clickOnEnter } from '$lib/helpers/a11y';
export let show = false;
@@ -23,7 +23,7 @@
function handleBLur(event: MouseEvent) {
if (event.target === backdrop) {
trackEvent('click_close_modal', {
trackEvent(Click.ModalCloseClick, {
from: 'backdrop'
});
closeModal();
@@ -38,7 +38,7 @@
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
trackEvent('click_close_modal', {
trackEvent(Click.ModalCloseClick, {
from: 'escape'
});
closeModal();
@@ -100,7 +100,7 @@
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
trackEvent(Click.ModalCloseClick, {
from: 'button'
})}
on:click={closeModal}>
@@ -152,5 +152,9 @@
:global() {
background-color: hsl(240 5% 8% / 0.6);
}
& :global(.modal-form) {
position: unset;
}
}
</style>
+226 -402
View File
@@ -1,31 +1,32 @@
<script lang="ts">
import { Id, ModalWrapper, Trim } from '.';
import { Button, Form } from '$lib/elements/forms';
import { Id, Trim } from '.';
import { Button } 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 { clickOnEnter } from '$lib/helpers/a11y';
import Empty from './empty.svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Typography } from '@appwrite.io/pink-svelte';
import DualTimeView from './dualTimeView.svelte';
import {
Layout,
Typography,
Modal,
ActionMenu,
Table,
Spinner,
ToggleButton,
Selector,
Empty,
Card
} from '@appwrite.io/pink-svelte';
import Form from '$lib/elements/forms/form.svelte';
import { IconCheck, IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
export let show: boolean;
export let mimeTypeQuery: string = 'image/';
@@ -43,7 +44,7 @@
selectedBucket = currentBucket?.$id;
});
function submitForm() {
function onSubmit() {
onSelect(currentFile);
closeModal();
}
@@ -74,6 +75,7 @@
}
function selectBucket(bucket: Models.Bucket | null) {
search.set('');
currentBucket = bucket;
selectedBucket = bucket?.$id ?? null;
resetFile();
@@ -108,6 +110,7 @@
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;
@@ -142,398 +145,219 @@
<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}
<Form {onSubmit}>
<Modal bind:open={show} title="Select file" size="l">
<Layout.Stack direction="row" height="50vh">
<aside>
<Typography.Caption variant="500">Buckets</Typography.Caption>
{#await buckets}
<div class="u-flex u-main-center">
<div class="loader" />
</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>
<DualTimeView
time={file.$createdAt} />
</TableCellText>
</TableRowButton>
{/each}
</TableBody>
</Table>
{/if}
{:else if $search}
<article
style:--card-bg-color="transparent"
style:--shadow-small="none"
style:--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"
--border="var(--color-neutral-15)">
<div class="common-section">
<div class="u-text-center common-section">
<Typography.Title size="s">
No files found within this bucket.
</Typography.Title>
<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}
{:then response}
<ActionMenu.Root>
{#each response.buckets as bucket}
{@const isSelected = bucket.$id === selectedBucket}
<ActionMenu.Item.Button
on:click={() => selectBucket(bucket)}
leadingIcon={isSelected ? IconCheck : undefined}>
{bucket.name}
</ActionMenu.Item.Button>
{:else}
<Empty
single
noMedia
--card-bg-color="transparent"
--shadow-small="none"
--border="var(--color-neutral-15)">
<div class="u-text-center u-flex-vertical u-cross-center u-gap-24">
<Typography.Title size="s">No buckets found</Typography.Title>
<ActionMenu.Item.Button>No buckets found</ActionMenu.Item.Button>
{/each}
</ActionMenu.Root>
{/await}
</aside>
<Layout.Stack>
{#await buckets then response}
{#if response?.total}
{#if currentBucket}
<Layout.Stack>
<Layout.Stack direction="row" alignItems="center">
<Typography.Title>{currentBucket?.name}</Typography.Title>
<Id value={currentBucket?.$id} event="bucket">
{currentBucket?.$id}
</Id>
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center">
<InputSearch
placeholder="Search files"
bind:value={$search}
disabled={!searchEnabled} />
<ToggleButton
bind:active={view}
buttons={[
{
id: 'list',
label: 'list view',
disabled: !searchEnabled,
icon: IconViewList
},
{
id: 'grid',
label: 'grid view',
disabled: !searchEnabled,
icon: IconViewGrid
}
]} />
<Button
secondary
external
href={`${base}/project-${$page.params.project}/storage`}>
Create bucket
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>
</Empty>
</Layout.Stack>
</Layout.Stack>
{#if files}
{#await files}
<Layout.Stack
justifyContent="center"
alignContent="center"
alignItems="center"
height="100%">
<Spinner size="l" />
<span>Loading files...</span>
</Layout.Stack>
{:then response}
{#if response?.files?.length}
{#if view === 'grid'}
<Layout.Grid
columnsXXS={1}
columnsXS={2}
columnsS={3}
columns={4}>
{#each response?.files as file}
<Card.Selector
group="files"
name="files"
value={file.$id}
src={getPreview(
currentBucket.$id,
file.$id,
360
)}
on:click={() => selectFile(file)} />
{/each}
</Layout.Grid>
{/if}
{#if view === 'list'}
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Filename</Table.Header.Cell>
<Table.Header.Cell width="140px">
ID
</Table.Header.Cell>
<Table.Header.Cell width="100px">
Type
</Table.Header.Cell>
<Table.Header.Cell width="100px">
Size
</Table.Header.Cell>
<Table.Header.Cell width="120px">
Created
</Table.Header.Cell>
</svelte:fragment>
{#each response?.files as file}
<Table.Button on:click={() => selectFile(file)}>
<Table.Cell>
<div
class="u-inline-flex u-cross-center u-gap-12">
<Selector.Radio
name="file"
group="file"
value={file.$id}
checked={file.$id ===
selectedFile} />
<img
style:border-radius="var(--border-radius-xsmall)"
width="28"
height="28"
src={getPreview(
currentBucket.$id,
file.$id
)}
alt={file.name} />
<Typography.Text truncate>
{file.name}
</Typography.Text>
</div>
</Table.Cell>
<Table.Cell>
<Id value={file.$id}>{file.$id}</Id>
</Table.Cell>
<Table.Cell>
{file.mimeType}
</Table.Cell>
<Table.Cell>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell>
<DualTimeView time={file.$createdAt} />
</Table.Cell>
</Table.Button>
{/each}
</Table.Root>
{/if}
{:else if $search}
<Empty
type="secondary"
title={`Sorry we couldn't find "${$search}"`}
description="There are no files that match your search.">
<Button
secondary
slot="actions"
on:click={() => ($search = '')}
>Clear search</Button>
</Empty>
{:else}
<Empty title="No files found within this bucket.">
<Button
secondary
slot="actions"
disabled={uploading}
on:click={() => fileSelector.click()}
>Upload file</Button>
</Empty>
{/if}
{/await}
{/if}
{/if}
{/await}
</article>
</div>
</div>
<div class="modal-footer u-margin-block-start-0">
<div class="u-flex u-main-end u-gap-16">
{:else}
<Empty title="No buckets found">
<Button
slot="actions"
secondary
external
href={`${base}/project-${$page.params.project}/storage`}>
Create bucket
</Button>
</Empty>
{/if}
{/await}
</Layout.Stack>
</Layout.Stack>
<svelte:fragment slot="footer">
<Layout.Stack direction="row" justifyContent="flex-end">
<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>
</Layout.Stack>
</svelte:fragment>
</Modal>
</Form>
+30 -48
View File
@@ -5,12 +5,11 @@
InputSelect,
InputText,
InputTags,
FormList,
InputSelectCheckbox,
InputDateTime
} from '$lib/elements/forms';
import { createEventDispatcher, onMount } from 'svelte';
import { tags, operators, addFilter, queries } from './store';
import { operators, addFilter, queries, type TagValue } from './store';
import type { Column } from '$lib/helpers/types';
import type { Writable } from 'svelte/store';
import { TagList } from '.';
@@ -43,6 +42,8 @@
$: isDisabled = !operator;
let localTags: TagValue[] = [];
onMount(() => {
value = column?.array ? [] : null;
if (column?.type === 'datetime') {
@@ -66,7 +67,7 @@
clear: void;
apply: { applied: number };
}>();
dispatch('apply', { applied: $tags.length });
dispatch('apply', { applied: localTags.length });
</script>
<div>
@@ -91,27 +92,24 @@
</Layout.Stack>
{#if column && operator && !operator?.hideInput}
{#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((e) => ({
label: e?.label ?? e,
value: e?.value ?? e,
checked: arrayValues.includes(e?.value ?? e)
}))}>
</InputSelectCheckbox>
{:else}
<InputTags
label="values"
showLabel={false}
id="value"
bind:tags={arrayValues}
placeholder="Enter values" />
{/if}
</FormList>
{#if column.format === 'enum'}
<InputSelectCheckbox
name="value"
bind:tags={arrayValues}
placeholder="Select value"
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e,
checked: arrayValues.includes(e?.value ?? e)
}))}>
</InputSelectCheckbox>
{:else}
<InputTags
label="values"
id="value"
bind:tags={arrayValues}
placeholder="Enter values" />
{/if}
{:else}
<ul class="u-margin-block-start-8">
{#if column.format === 'enum'}
@@ -122,9 +120,7 @@
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e
}))}
label="Value"
showLabel={false} />
}))} />
{:else if column.type === 'integer' || column.type === 'double'}
<InputNumber id="value" bind:value placeholder="Enter value" />
{:else if column.type === 'boolean'}
@@ -139,12 +135,7 @@
bind:value />
{:else if column.type === 'datetime'}
{#key value}
<InputDateTime
id="value"
bind:value
label="value"
showLabel={false}
step={60} />
<InputDateTime id="value" bind:value step={60} />
{/key}
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
@@ -162,21 +153,12 @@
{#if !singleCondition}
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
<TagList />
<TagList
tags={localTags}
on:remove={(e) => {
queries.removeFilter(e.detail);
queries.apply();
}} />
</ul>
{/if}
</div>
<style lang="scss">
.selects {
:global(> *) {
flex: 1;
}
}
.tags {
:global(b) {
font-weight: bold;
}
}
</style>
@@ -0,0 +1,20 @@
<script lang="ts">
import { ActionMenu } from '@appwrite.io/pink-svelte';
import FiltersModal from './filtersModal.svelte';
import type { Writable } from 'svelte/store';
import type { Column } from '$lib/helpers/types';
export let columns: Writable<Column[]>;
let show = false;
</script>
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => {
show = true;
}}>Custom filters</ActionMenu.Item.Button>
</ActionMenu.Root>
{#if show}
<FiltersModal bind:show {columns} analyticsSource="custom-filters" />
{/if}
+11 -22
View File
@@ -16,6 +16,7 @@
import { createEventDispatcher } from 'svelte';
import { Icon, Layout, Popover } from '@appwrite.io/pink-svelte';
import { IconFilter, IconFilterLine } from '@appwrite.io/pink-icons-svelte';
import { Click, Submit, trackEvent } from '$lib/actions/analytics';
export let query = '[]';
export let columns: Writable<Column[]>;
@@ -25,6 +26,7 @@
export let clearOnClick = false; // When enabled the user doesn't have to click apply to clear the filters
export let enableApply = false;
export let quickFilters = false;
export let analyticsSource = '';
let displayQuickFilters = quickFilters;
const dispatch = createEventDispatcher();
@@ -53,12 +55,14 @@
selectedColumn = null;
queries.clearAll();
if (clearOnClick) {
trackEvent(Submit.FilterClear, { source: analyticsSource });
queries.apply();
}
}
function apply() {
if (quickFilters && displayQuickFilters) {
trackEvent(Submit.FilterApply, { source: analyticsSource });
dispatch('apply');
} else if (
selectedColumn &&
@@ -118,7 +122,13 @@
<div class="is-not-mobile">
<Popover let:toggle placement="bottom-start">
<Button secondary on:click={toggle} {disabled}>
<Button
secondary
on:click={(event) => {
toggle(event);
trackEvent(Click.FilterApplyClick, { source: analyticsSource });
}}
{disabled}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
{#if applied > 0}
@@ -225,24 +235,3 @@
</svelte:fragment>
</Modal>
</div>
<style lang="scss">
.dropped {
border-radius: 0.5rem;
box-shadow: 0px 16px 32px 0px rgba(55, 59, 77, 0.04);
padding: 1rem;
margin-top: 0.5rem;
width: 37.5rem;
hr {
height: 1px;
width: calc(100% + 2rem);
background-color: hsl(var(--border));
margin-block-start: 1rem;
margin-inline: -1rem;
}
}
</style>
@@ -0,0 +1,180 @@
<script lang="ts">
import { Submit, trackEvent } from '$lib/actions/analytics';
import {
Button,
InputDateTime,
InputNumber,
InputSelect,
InputSelectCheckbox,
InputTags,
InputText
} from '$lib/elements/forms';
import type { Writable } from 'svelte/store';
import Modal from '../modal.svelte';
import { addFilter, generateTag, operators, queries, type TagValue } from './store';
import type { Column } from '$lib/helpers/types';
import { TagList } from '.';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
export let show = false;
export let columns: Writable<Column[]>;
export let analyticsSource = '';
export let clearOnClick = false;
/* eslint @typescript-eslint/no-explicit-any: 'off' */
let value: any = null;
let selectedColumn: string | null = null;
let operatorKey: string | null = null;
let arrayValues: string[] = [];
let localTags: TagValue[] = [];
let localQueries: {
id: string;
operator: string;
value: any;
arrayValues: string[];
}[] = [];
function apply() {
localQueries.forEach((query) => {
addFilter($columns, query.id, query.operator, query.value, query.arrayValues);
});
queries.apply();
localTags = [];
localQueries = [];
trackEvent(Submit.FilterApply, {
source: analyticsSource,
filters: localTags
});
show = false;
}
function addCondition() {
const newTag = generateTag(selectedColumn, operatorKey, value, arrayValues);
if (localTags.some((t) => t.tag === newTag.tag && t.value === newTag.value)) {
return;
} else {
localQueries = [
...localQueries,
{
id: selectedColumn,
operator: operatorKey,
value: value,
arrayValues: arrayValues
}
];
localTags = [...localTags, newTag];
}
}
function removeCondition(tag: TagValue) {
localTags = localTags.filter((t) => t !== tag);
}
function clearAll() {
localTags = [];
localQueries = [];
}
$: column = $columns.find((c) => c.id === selectedColumn) as Column;
$: operatorsForColumn = Object.entries(operators)
.filter(([, v]) => v.types.includes(column?.type))
.map(([k]) => ({
label: k,
value: k
}));
</script>
<Modal title="Filters" bind:show>
<span slot="description"> Apply filter rules to refine the table view </span>
<Layout.Stack>
<Layout.Stack gap="s" direction="row" alignItems="flex-start">
<InputSelect
id="column"
options={$columns
.filter((c) => c.filter !== false)
.map((c) => ({
label: c.title,
value: c.id
}))}
placeholder="Select column"
bind:value={selectedColumn} />
<InputSelect
id="operator"
disabled={!column}
options={operatorsForColumn}
placeholder="Select operator"
bind:value={operatorKey} />
</Layout.Stack>
{#if column && operatorKey}
{#if column?.array}
{#if column.format === 'enum'}
<InputSelectCheckbox
name="value"
bind:tags={arrayValues}
placeholder="Select value"
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e,
checked: arrayValues.includes(e?.value ?? e)
}))}>
</InputSelectCheckbox>
{:else}
<InputTags
label="values"
id="value"
bind:tags={arrayValues}
placeholder="Enter values" />
{/if}
{:else if column.format === 'enum'}
<InputSelect
id="value"
bind:value
placeholder="Select value"
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e
}))} />
{: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 if column.type === 'datetime'}
{#key value}
<InputDateTime id="value" bind:value step={60} />
{/key}
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
{/if}
{/if}
<div>
<Button text on:click={addCondition}>
<Icon icon={IconPlus} slot="start" size="s" />
Add condition
</Button>
</div>
</Layout.Stack>
{#if localTags?.length}
<Layout.Stack direction="row" gap="s" alignItems="center" wrap="wrap">
<TagList tags={localTags} on:remove={(e) => removeCondition(e.detail)} />
</Layout.Stack>
{/if}
<svelte:fragment slot="footer">
{#if localTags?.length}
<Button size="s" text on:click={clearAll}>Clear all</Button>
{:else}
<Button size="s" text on:click={() => (show = false)}>Cancel</Button>
{/if}
<Button size="s" on:click={apply} disabled={!localTags?.length}>Apply</Button>
</svelte:fragment>
</Modal>
+3
View File
@@ -1,3 +1,6 @@
export { default as Filters } from './filters.svelte';
export { default as TagList } from './tagList.svelte';
export { default as CustomFilters } from './customFilters.svelte';
export { default as QuickFilters } from './quickFilters.svelte';
export { default as ParsedTagList } from './parsedTagList.svelte';
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
@@ -0,0 +1,42 @@
<script lang="ts">
import { Icon, Layout, Tag, Tooltip } from '@appwrite.io/pink-svelte';
import { queries, tagFormat, tags } from './store';
import { IconX } from '@appwrite.io/pink-icons-svelte';
import { parsedTags } from './setFilters';
import { Button } from '$lib/elements/forms';
</script>
{#if $parsedTags?.length}
<Layout.Stack direction="row" gap="s" wrap="wrap" alignItems="center" inline>
{#each $parsedTags as tag}
<span>
<Tooltip
disabled={Array.isArray(tag.value) ? tag.value?.length < 3 : true}
maxWidth="600px">
<Tag
size="s"
on:click={() => {
const t = $tags.filter((t) => t.tag.includes(tag.tag.split(' ')[0]));
t.forEach((t) => (t ? queries.removeFilter(t) : null));
queries.apply();
parsedTags.update((tags) => tags.filter((t) => t.tag !== tag.tag));
}}>
{#key tag.tag}
<span use:tagFormat>{tag.tag}</span>
{/key}
<Icon icon={IconX} size="s" slot="end" />
</Tag>
<span slot="tooltip">{tag?.value?.toString()}</span>
</Tooltip>
</span>
{/each}
<Button
size="s"
text
on:click={() => {
queries.clearAll();
queries.apply();
parsedTags.set([]);
}}>Clear all</Button>
</Layout.Stack>
{/if}
@@ -0,0 +1,208 @@
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { queryParamToMap } from '$lib/components/filters/store';
import type { Column } from '$lib/helpers/types';
import { type Writable } from 'svelte/store';
import { CustomFilters } from '$lib/components/filters';
import { addFilterAndApply, buildFilterCol, type FilterData } from './quickFilters';
import { parsedTags, setFilters } from './setFilters';
import Menu from '../menu/menu.svelte';
import { Button } from '$lib/elements/forms';
import { Icon } from '@appwrite.io/pink-svelte';
import {
IconChevronLeft,
IconChevronRight,
IconFilterLine
} from '@appwrite.io/pink-icons-svelte';
import QuickfiltersSubMenu from './quickfiltersSubMenu.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { BottomSheet } from '..';
import { capitalize } from '$lib/helpers/string';
export let columns: Writable<Column[]>;
export let analyticsSource: string;
let openBottomSheet = false;
let filterCols = $columns
.map((col) => (col.filter !== false ? buildFilterCol(col) : null))
.filter((f) => f?.options);
afterNavigate((p) => {
const paramQueries = p.to.url.searchParams.get('query');
const localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
// console.log(paramQueries, localQueries, localTags);
setFilters(localTags, filterCols, $columns);
filterCols = filterCols;
});
$: subSheets = filterCols.map((col) => {
return {
title: col.title,
top: {
title: col.title,
trailingIcon: IconChevronRight,
items: col.options.map((o) => {
return {
title: capitalize(o.label),
name: capitalize(o.label),
options: col.options,
checked: o.checked,
onClick: () => {
addFilterAndApply(
col.id,
col.title,
col.operator,
o.value,
generateFilterArrayValue(col, o.value),
$columns,
analyticsSource
);
subSheets = subSheets;
}
};
})
},
bottom: {
name: 'Back',
items: [
{
name: 'Back',
leadingIcon: IconChevronLeft,
navigatePrevious: true,
onClick: () => {
// navigate to the previous menu
}
}
]
}
};
});
$: organizationsBottomSheet = {
top: {
title: 'Filters',
items: filterCols.map((col) => {
return {
name: col.title,
onClick: () =>
console.log(subSheets.find((sheet) => sheet?.title === col?.title)),
subMenu: subSheets.find((sheet) => sheet?.title === col?.title),
trailingIcon: IconChevronRight
};
})
},
bottom: {
name: 'Clear All',
items: [
{
name: 'Clear All',
onClick: () => {
filterCols.forEach((col) => {
addFilterAndApply(
col.id,
col.title,
col.operator,
null,
[],
$columns,
analyticsSource
);
});
}
}
]
}
};
function generateFilterArrayValue(col: FilterData, value: string) {
if (!col?.array) return [];
if (col.options?.find((opt) => opt.value === value)?.checked) {
return col.options
?.filter((opt) => opt?.checked)
.map((opt) => opt.value)
.filter((item) => item !== value);
} else {
let arrayValue =
col.options?.filter((opt) => opt?.checked)?.map((opt) => opt.value) ?? [];
arrayValue = [...arrayValue, value];
return arrayValue;
}
}
</script>
{#if $isSmallViewport}
{#if $parsedTags?.length}
<Button
secondary
badge={`${$parsedTags?.length}`}
on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{/if}
{:else}
<Menu>
{#if $parsedTags?.length}
<Button secondary badge={`${$parsedTags?.length}`}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{/if}
<svelte:fragment slot="menu">
{#each filterCols as filter}
{#if filter.options}
<QuickfiltersSubMenu
{filter}
variant={filter?.array ? 'checkbox' : 'radio'}
on:add={(e) => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
e.detail.value,
filter?.array
? (filter.options
.filter((opt) => opt.checked)
.map((opt) => opt.value) ?? [])
: [],
$columns,
analyticsSource
);
}}
on:clear={() => {
addFilterAndApply(
filter.id,
filter.title,
filter.operator,
null,
[],
$columns,
analyticsSource
);
}} />
{/if}
{/each}
</svelte:fragment>
<svelte:fragment slot="end">
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
{/if}
{#if $isSmallViewport && openBottomSheet}
<BottomSheet.Menu bind:isOpen={openBottomSheet} menu={organizationsBottomSheet} />
{/if}
@@ -0,0 +1,77 @@
import type { Column } from '$lib/helpers/types';
import { get } from 'svelte/store';
import { addFilter, queries, tags, ValidOperators } from './store';
import { Submit, trackEvent } from '$lib/actions/analytics';
export type FilterData = {
title: string;
id: string;
array: boolean;
show: boolean;
tag: string;
operator: ValidOperators;
options: { value: string; label: string; checked: boolean }[];
};
export function buildFilterCol(col: Column, customOperator = null): FilterData {
return {
title: col.title,
id: col.id,
show: false,
array: col?.array,
tag: null,
operator: customOperator ?? ValidOperators.Equal,
options: col?.elements?.map((element) => {
return {
value: (element?.value ?? element) as string,
label: (element?.label ?? element) as string,
checked: false
};
})
};
}
export function addFilterAndApply(
colId: string,
colTitle: string,
operator: ValidOperators,
value: string,
arrayValues: string[] = [],
columns: Column[],
analyticsSource: string
) {
const tagList = get(tags).filter((tag) => tag.tag.includes(colTitle));
tagList.forEach((tag) => queries.removeFilter(tag));
if (value || arrayValues?.length) {
if (colId === 'sourceSize' || colId === 'buildSize') {
addSizeFilter(value, colId, columns);
} else if (colId === 'statusCode') {
addStatusCodeFilter(value, colId, columns);
} else if (colId === '$createdAt' || colId === '$updatedAt' || colId === 'buildDuration') {
addDateFilter(value, colId, columns);
} else {
addFilter(columns, colId, operator, value, arrayValues);
}
}
queries.apply();
trackEvent(Submit.ApplyQuickFilter, {
source: analyticsSource,
column: colId,
value: value || arrayValues.join(', ')
});
}
export function addStatusCodeFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.LessThanOrEqual, parseInt(value));
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, parseInt(value) - 99);
}
export function addDateFilter(value: string, colId: string, columns: Column[]) {
const now = new Date();
const isoValue = new Date(now.getTime() - parseInt(value));
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, isoValue.toISOString());
addFilter(columns, colId, ValidOperators.LessThanOrEqual, now.toISOString());
}
export function addSizeFilter(value: string, colId: string, columns: Column[]) {
addFilter(columns, colId, ValidOperators.GreaterThanOrEqual, value);
}
@@ -0,0 +1,102 @@
<script lang="ts">
import { capitalize } from '$lib/helpers/string';
import { IconChevronRight } from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Card, Layout, Selector } from '@appwrite.io/pink-svelte';
import { createMenubar, melt } from '@melt-ui/svelte';
import { createEventDispatcher } from 'svelte';
import type { FilterData } from './quickFilters';
export let filter: FilterData;
export let variant: 'checkbox' | 'radio' = 'checkbox';
const {
builders: { createMenu }
} = createMenubar();
const {
elements: { item: item, separator: separator },
builders: { createSubmenu: createSubmenu, createMenuRadioGroup, createCheckboxItem }
} = createMenu();
const {
elements: { checkboxItem: checkboxItem }
} = createCheckboxItem();
const {
elements: { radioGroup: radioGroup }
} = createMenuRadioGroup({});
const {
elements: { subMenu: subMenu, subTrigger: subTrigger }
} = createSubmenu();
const dispatch = createEventDispatcher();
</script>
<div use:melt={$subTrigger}>
<ActionMenu.Root noPadding>
<ActionMenu.Item.Button trailingIcon={IconChevronRight}
>{filter.title}</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
<div class="menu subMenu" use:melt={$subMenu}>
<Card.Base padding="xxxs">
<div use:melt={$radioGroup}>
{#each filter.options as option (option.value + option.checked)}
{#if variant === 'radio'}
<div use:melt={$item}>
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => {
option.checked = !option.checked;
dispatch('add', {
value: option.value
});
}}>
{capitalize(option.label)}
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
{:else}
<div use:melt={$checkboxItem}>
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => {
option.checked = !option.checked;
dispatch('add', {
value: option.checked
});
}}>
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox checked={option.checked} size="s" />
{capitalize(option.label)}
</Layout.Stack>
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
{/if}
{/each}
{#if filter.options.some((option) => option.checked)}
<div class="separator" use:melt={$separator} />
<div use:melt={$item}>
<ActionMenu.Root>
<ActionMenu.Item.Button
on:click={() => {
dispatch('clear');
}}>
Clear all
</ActionMenu.Item.Button>
</ActionMenu.Root>
</div>
{/if}
</div>
</Card.Base>
</div>
<style>
.menu {
min-width: 244px;
z-index: 20;
}
</style>
+187
View File
@@ -0,0 +1,187 @@
import type { Column } from '$lib/helpers/types';
import { get, writable } from 'svelte/store';
import { type FilterData } from './quickFilters';
import { tags, type TagValue } from './store';
export const parsedTags = writable<TagValue[]>([]);
export function setFilters(localTags: TagValue[], filterCols: FilterData[], $columns: Column[]) {
if (!localTags?.length) {
filterCols.forEach((filter) => {
resetOptions(filter);
cleanOldTags(filter.title);
});
} else {
filterCols.forEach((filter) => {
if (filter.id.toLowerCase().includes('duration')) {
setTimeFilter(filter, $columns);
} else if (filter.id.toLocaleLowerCase().includes('size')) {
setSizeFilter(filter, $columns);
} else if (filter.id.toLocaleLowerCase().includes('statuscode')) {
setStatusCodeFilter(filter, $columns);
} else if (filter.id === '$createdAt' || filter.id === '$updatedAt') {
setDateFilter(filter, $columns);
} else {
setFilterData(filter);
}
});
// Reasinging the filters to trigger reactivity
filterCols = filterCols;
}
}
export function setFilterData(filter: FilterData) {
const tagData = get(tags).find((tag) => tag.tag.includes(`**${filter.title}**`));
if (tagData) {
if (Array.isArray(tagData.value) && tagData.value?.length) {
const values = tagData.value as string[];
filter.options.forEach((option) => {
option.checked = values.includes(option.value);
});
}
cleanOldTags(filter.title);
const newTag = {
tag: tagData.tag.replace(',', ' or '),
value: tagData.value
};
parsedTags.update((tags) => {
tags.push(newTag);
return tags;
});
} else {
resetOptions(filter);
cleanOldTags(filter.title);
}
}
export function setTimeFilter(filter: FilterData, columns: Column[]) {
const col = columns.find((c) => c.id === filter.id);
const timeTag = get(tags).find((tag) => tag.tag.includes(`**${filter.title}**`));
if (timeTag) {
const now = new Date();
const diff = now.getTime() - new Date(timeTag.value as string).getTime();
const ranges = col.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
const newTag = {
tag: `**${filter.title}** is **${dateRange.label}**`,
value: timeTag.value
};
cleanOldTags(filter.title);
parsedTags.update((tags) => {
tags.push(newTag);
return tags;
});
}
} else {
cleanOldTags(filter.title);
}
}
export function setSizeFilter(filter: FilterData, columns: Column[]) {
const sizeTag = get(tags).find((tag) => tag.tag.includes(`**${filter.title}**`));
const col = columns.find((c) => c.id === filter.id);
if (sizeTag) {
const size = sizeTag.value as string;
const ranges = col.elements as { value: string; label: string }[];
// find smallest range that is bigger than size
const sizeRange = ranges.reduce((prev, curr) => {
if (parseInt(size) >= parseInt(curr.value)) {
return curr;
}
return prev;
});
if (sizeRange) {
cleanOldTags(filter.title);
const newTag = {
tag: `**${filter.title}** is **${sizeRange.label}**`,
value: sizeTag.value
};
parsedTags.update((tags) => {
tags.push(newTag);
return tags;
});
}
} else {
cleanOldTags(filter.title);
}
}
export function setStatusCodeFilter(filter: FilterData, columns: Column[]) {
const statusCodeTag = get(tags).find((tag) => tag.tag.includes(`**${filter.title}**`));
const col = columns.find((c) => c.id === filter.id);
if (statusCodeTag) {
const ranges = col.elements as { value: number; label: string }[];
const codeRange = ranges.find((c) => c?.value && c.value === statusCodeTag.value);
if (codeRange) {
cleanOldTags(filter.title);
const newTag = {
tag: `**${filter.title}** is **${codeRange.label}**`,
value: statusCodeTag.value
};
console.log(codeRange);
parsedTags.update((tags) => {
tags.push(newTag);
return tags;
});
}
} else {
cleanOldTags(filter.title);
}
}
export function setDateFilter(filter: FilterData, columns: Column[]) {
const dateTag = get(tags).find((tag) => tag.tag.includes(`**${filter.title}**`));
const col = columns.find((c) => c.id === filter.id);
if (dateTag) {
const now = new Date();
const diff = now.getTime() - new Date(dateTag.value as string).getTime();
const ranges = col.elements as { value: string; label: string }[];
const dateRange = ranges.reduce((prev, curr) => {
if (parseInt(curr.value) < diff && curr.value > prev.value) {
return curr;
}
return prev;
});
if (dateRange) {
cleanOldTags(filter.title);
const newTag = {
tag: `**${filter.title}** is **${dateRange.label}**`,
value: dateTag.value
};
parsedTags.update((tags) => {
tags.push(newTag);
return tags;
});
}
} else {
cleanOldTags(filter.title);
}
}
function cleanOldTags(title: string) {
parsedTags.update((tags) => {
tags = tags.filter((tag) => !tag.tag.includes(`**${title}**`));
return tags;
});
}
export function resetOptions(filter: FilterData) {
filter.options.forEach((option) => {
option.checked = false;
});
}
+23 -20
View File
@@ -1,5 +1,4 @@
import { goto } from '$app/navigation';
import { derived, get, writable } from 'svelte/store';
import { page } from '$app/stores';
import deepEqual from 'deep-equal';
@@ -245,30 +244,13 @@ function formatArray(array: string[]) {
}
}
function generateDefaultOperators() {
export function generateDefaultOperators() {
const operators: Record<string, Operator> = {};
operatorsDefault.forEach((operator, operatorName) => {
operators[operatorName] = {
toQuery: operator.query,
toTag: (attribute, input = null, type = null) => {
if (input === null) {
return {
value: '',
tag: `**${attribute}** ${operatorName}`
};
} else if (Array.isArray(input) && input.length > 2) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
};
} else if (type === ValidTypes.Datetime) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
};
} else {
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
}
return generateTag(attribute, operatorName, input, type);
},
types: operator.types,
hideInput: operator.hideInput
@@ -277,6 +259,27 @@ function generateDefaultOperators() {
return operators;
}
export function generateTag(attribute: string, operatorName: string, input = null, type = null) {
if (input === null) {
return {
value: '',
tag: `**${attribute}** ${operatorName}`
};
} else if (Array.isArray(input) && input.length > 2) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
};
} else if (type === ValidTypes.Datetime) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
};
} else {
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
}
}
export const operators = generateDefaultOperators();
export function tagFormat(node: HTMLElement) {
+25 -18
View File
@@ -1,22 +1,29 @@
<script lang="ts">
import { queries, tagFormat, tags } from './store';
import { Tooltip } from '@appwrite.io/pink-svelte';
import { IconX } from '@appwrite.io/pink-icons-svelte';
import { tagFormat, type TagValue } from './store';
import { Icon, Tag, Tooltip } from '@appwrite.io/pink-svelte';
import { createEventDispatcher } from 'svelte';
export let tags: TagValue[];
const dispatch = createEventDispatcher();
</script>
{#each $tags as tag (tag)}
<Tooltip disabled={Array.isArray(tag.value) ? tag.value?.length < 3 : true}>
<button
type="button"
class="tag"
on:click|preventDefault={() => {
queries.removeFilter(tag);
queries.apply();
}}>
<span class="text" use:tagFormat>
{tag.tag}
</span>
<i class="icon-x" />
</button>
<span slot="tooltip">{tag?.value?.toString()}</span>
</Tooltip>
{#each tags as tag (tag)}
<span>
<Tooltip
disabled={Array.isArray(tag.value) ? tag.value?.length < 3 : true}
maxWidth="600px">
<Tag
size="s"
on:click={() => {
dispatch('remove', tag);
}}>
{#key tag.tag}
<span use:tagFormat>{tag.tag}</span>
{/key}
<Icon icon={IconX} size="s" slot="end" />
</Tag>
<span slot="tooltip">{tag?.value?.toString()}</span>
</Tooltip>
</span>
{/each}
@@ -3,8 +3,8 @@
import { consoleVariables } from '$routes/(console)/store';
import { Layout } from '@appwrite.io/pink-svelte';
export let connectBehaviour: 'now' | 'later' = 'now';
const isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
export let connectBehaviour: 'now' | 'later' = isVcsEnabled ? 'now' : 'later';
</script>
<Layout.Grid columns={2} columnsXS={1}>
@@ -15,7 +15,7 @@
title="Connect your repository">
Clone this template into a new Git repository or link it to an existing one.
</LabelCard>
<LabelCard value="later" bind:group={connectBehaviour} disabled={!isVcsEnabled}>
<LabelCard value="later" bind:group={connectBehaviour}>
<svelte:fragment slot="title">Connect later</svelte:fragment>
Deploy now and connect your version control later via CLI or Git integration in your settings.
</LabelCard>
+37 -43
View File
@@ -1,54 +1,48 @@
<script lang="ts">
import { page } from '$app/stores';
import Button from '$lib/elements/forms/button.svelte';
import { sdk } from '$lib/stores/sdk';
import { consoleVariables } from '$routes/(console)/store';
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
import { Card, Empty, Icon } from '@appwrite.io/pink-svelte';
import Alert from '../alert.svelte';
import { Alert, Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte';
import { isSelfHosted } from '$lib/system';
import { connectGitHub } from '$lib/stores/git';
export let callbackState: Record<string, string> = null;
let isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
function connectGitHub() {
const redirect = new URL($page.url);
if (callbackState) {
Object.keys(callbackState).forEach((key) => {
redirect.searchParams.append(key, callbackState[key]);
});
}
const target = new URL(`${sdk.forProject.client.config.endpoint}/vcs/github/authorize`);
target.searchParams.set('project', $page.params.project);
target.searchParams.set('success', redirect.toString());
target.searchParams.set('failure', redirect.toString());
target.searchParams.set('mode', 'admin');
return target;
}
</script>
{#if !isVcsEnabled && isSelfHosted}
<Alert>
<span slot="title"> Installing Git on a self-hosted instance </span>
<p>
Before installing Git in a locally hosted Appwrite project, ensure your environment
variables are configured.
</p>
<svelte:fragment slot="buttons">
<Button secondary external href="#/">Learn more</Button>
</svelte:fragment>
</Alert>
{/if}
<Card.Base padding="none" border="dashed">
<Empty
type="secondary"
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
<svelte:fragment slot="actions">
<Button secondary href={connectGitHub().toString()} disabled={!isVcsEnabled}>
<Icon slot="start" icon={IconGithub} />
Connect to GitHub
</Button>
</svelte:fragment>
</Empty>
</Card.Base>
<Layout.Stack>
{#if !isVcsEnabled && isSelfHosted}
<Alert.Inline status="info" title="Installing Git on a self-hosted instance ">
<Layout.Stack>
<p>
Before installing Git in a locally hosted Appwrite project, ensure your
environment variables are configured.
</p>
<div>
<Button
compact
external
href="https://appwrite.io/docs/advanced/self-hosting/functions#git"
>Learn more</Button>
</div>
</Layout.Stack>
</Alert.Inline>
{/if}
<Card.Base padding="none" border="dashed">
<Empty
type="secondary"
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
<svelte:fragment slot="actions">
<Button
secondary
href={connectGitHub(callbackState).toString()}
disabled={!isVcsEnabled}>
<Icon slot="start" icon={IconGithub} />
Connect to GitHub
</Button>
</svelte:fragment>
</Empty>
</Card.Base>
</Layout.Stack>
@@ -10,7 +10,7 @@
</script>
<Layout.Stack gap="xxs" direction="row" alignItems="center">
{#if domains.total}
{#if domains?.total}
<Link external href={`${$protocol}${domains.rules[0]?.domain}`} variant="muted">
<Layout.Stack gap="xxs" direction="row" alignItems="center">
<Trim alternativeTrim>
@@ -16,7 +16,7 @@
{#if deployment.type === 'vcs'}
<Popover padding="none" let:toggle>
<Layout.Stack>
<div>
<Link
on:click={(e) => {
e.preventDefault();
@@ -26,7 +26,7 @@
<Icon icon={IconGithub} size="s" /> GitHub
</Layout.Stack>
</Link>
</Layout.Stack>
</div>
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<ActionMenu.Item.Anchor
+5
View File
@@ -2,3 +2,8 @@ export { default as Repositories } from './repositories.svelte';
export { default as ConnectGit } from './connectGit.svelte';
export { default as NewRepository } from './newRepository.svelte';
export { default as RepositoryBehaviour } from './repositoryBehaviour.svelte';
export { default as DeploymentCreatedBy } from './deploymentCreatedBy.svelte';
export { default as DeploymentSource } from './deploymentSource.svelte';
export { default as DeploymentDomains } from './deploymentDomains.svelte';
export { default as ConnectBehaviour } from './connectBehaviour.svelte';
export { default as ProductionBranchFieldset } from './productionBranchFieldset.svelte';
@@ -1,45 +1,92 @@
<script lang="ts">
import { Button, InputSelect, InputText } from '$lib/elements/forms';
import { Fieldset, Layout, Selector } from '@appwrite.io/pink-svelte';
import SelectRootModal from '../../../routes/(console)/project-[project]/sites/(components)/selectRootModal.svelte';
import { Fieldset, Layout, Selector, Skeleton } from '@appwrite.io/pink-svelte';
import SelectRootModal from './selectRootModal.svelte';
import { sdk } from '$lib/stores/sdk';
import { sortBranches } from '$lib/stores/vcs';
export let branch: string;
export let rootDir: string;
export let options: { value: string; label: string }[] = [];
export let silentMode: boolean;
export let installationId: string;
export let repositoryId: string;
let show = false;
async function loadBranches() {
const { branches } = await sdk.forProject.vcs.listRepositoryBranches(
installationId,
repositoryId
);
const sorted = sortBranches(branches);
branch = sorted[0]?.name ?? null;
if (!branch) {
branch = 'main';
}
return sorted;
}
</script>
<Fieldset legend="Branch">
<Layout.Stack gap="xl">
<InputSelect
required
id="branch"
label="Production branch"
placeholder="Select branch"
isSearchable
bind:value={branch}
on:select={(event) => {
branch = event.detail.value;
}}
{options} />
<Layout.Stack direction="row" gap="s" alignItems="flex-end">
<InputText
id="root"
label="Root directory"
placeholder="Select directory"
bind:value={rootDir} />
<Button secondary size="s" on:click={() => (show = true)}>Select</Button>
{#await loadBranches()}
<Layout.Stack gap="xl">
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width="100%" height={32} />
</Layout.Stack>
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width="100%" height={32} />
</Layout.Stack>
<Layout.Stack gap="xs">
<Skeleton variant="line" width={100} height={20} />
<Skeleton variant="line" width={300} height={15} />
<Skeleton variant="line" width={300} height={15} />
</Layout.Stack>
</Layout.Stack>
{:then branches}
{@const options =
branches
?.map((branch) => {
return {
value: branch.name,
label: branch.name
};
})
?.sort((a, b) => {
return a.label > b.label ? 1 : -1;
}) ?? []}
<Layout.Stack gap="xl">
<InputSelect
required
id="branch"
label="Production branch"
placeholder="Select branch"
isSearchable
bind:value={branch}
on:select={(event) => {
branch = event.detail.value;
}}
{options} />
<Layout.Stack direction="row" gap="s" alignItems="flex-end">
<InputText
id="root"
label="Root directory"
placeholder="Select directory"
bind:value={rootDir} />
<Button secondary size="s" on:click={() => (show = true)}>Select</Button>
</Layout.Stack>
<Selector.Checkbox
size="s"
id="silentMode"
label="Silent mode"
description="If selected, comments will not be created when pushing changes to this repository."
bind:checked={silentMode} />
</Layout.Stack>
<Selector.Checkbox
size="s"
id="silentMode"
label="Silent mode"
description="If selected, comments will not be created when pushing changes to this repository."
bind:checked={silentMode} />
</Layout.Stack>
{/await}
</Fieldset>
{#if show}
+101 -84
View File
@@ -1,9 +1,7 @@
<script lang="ts">
import { base } from '$app/paths';
import { EmptySearch } from '$lib/components';
import { EmptySearch, Paginator } from '$lib/components';
import { Button, InputSearch, InputSelect } from '$lib/elements/forms';
import { timeFromNow } from '$lib/helpers/date';
import { app } from '$lib/stores/app';
import { sdk } from '$lib/stores/sdk';
import { repositories } from '$routes/(console)/project-[project]/functions/function-[function]/store';
import { installation, installations, repository } from '$lib/stores/vcs';
@@ -21,6 +19,7 @@
import ConnectGit from './connectGit.svelte';
import SvgIcon from '../svgIcon.svelte';
import { getFrameworkIcon } from '$routes/(console)/project-[project]/sites/store';
import { VCSDetectionType, type Models } from '@appwrite.io/console';
const dispatch = createEventDispatcher();
@@ -31,11 +30,13 @@
export let installationList = $installations;
export let product: 'functions' | 'sites' = 'functions';
let search = '';
let selectedInstallation = null;
$: {
hasInstallations = installationList?.total > 0;
}
let selectedInstallation = null;
async function loadInstallations() {
if (installationList) {
if (installationList.installations.length) {
@@ -57,16 +58,30 @@
}
}
let search = '';
async function loadRepositories(installationId: string, search: string) {
if (
!$repositories ||
$repositories.installationId !== installationId ||
$repositories.search !== search
) {
$repositories.repositories = (
await sdk.forProject.vcs.listRepositories(installationId, search || undefined)
).providerRepositories;
//TODO: remove forced cast after backend fixes
if (product === 'functions') {
$repositories.repositories = (
(await sdk.forProject.vcs.listRepositories(
installationId,
VCSDetectionType.Runtime,
search || undefined
)) as unknown as Models.ProviderRepositoryRuntimeList
).runtimeProviderRepositories;
} else {
$repositories.repositories = (
await sdk.forProject.vcs.listRepositories(
installationId,
VCSDetectionType.Framework,
search || undefined
)
).frameworkProviderRepositories;
}
}
$repositories.search = search;
@@ -77,13 +92,7 @@
$repository = $repositories.repositories[0];
}
return $repositories.repositories.slice(0, 4);
}
async function detectFramework(repo) {
console.log(repo);
// TODO add code once backend is implemented
return '';
return $repositories.repositories;
}
</script>
@@ -145,79 +154,87 @@
</Table.Root>
{:then response}
{#if response?.length}
<Table.Root>
{#each response as repo}
<Table.Row>
<Table.Cell>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
on:change={() => repository.set(repo)}
value={repo.id} />
{/if}
{#if product === 'sites'}
{#await detectFramework(repo)}
<Avatar size="xs" alt={repo.name} empty />
{:then framework}
<Avatar
size="xs"
alt={repo.name}
empty={!framework}>
<SvgIcon name={getFrameworkIcon(framework)} />
</Avatar>
{/await}
{:else}
<Avatar
size="xs"
src={repo?.runtime
? `${base}/icons/${$app.themeInUse}/color/${
repo.runtime.split('-')[0]
}.svg`
: ''}
alt={repo.name} />
{/if}
<Layout.Stack gap="s" direction="row" alignItems="center">
<Typography.Text
truncate
color="--fgcolor-neutral-secondary">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
<Paginator
items={response}
let:paginatedItems
hideFooter={response?.length <= 6}
limit={6}>
<Table.Root>
{#each paginatedItems as repo}
<Table.Row>
<Table.Cell>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
on:change={() => repository.set(repo)}
value={repo.id} />
{/if}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
{#if product === 'sites'}
{#if repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<SvgIcon
name={getFrameworkIcon(repo.framework)}
iconSize="small" />
</Avatar>
{:else}
<Avatar size="xs" alt={repo.name} empty />
{/if}
{:else}
{@const iconName = repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar size="xs" alt={repo.name} empty={!iconName}>
<SvgIcon name={iconName} iconSize="small" />
</Avatar>
{/if}
<Layout.Stack
gap="s"
direction="row"
alignItems="center">
<Typography.Text
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
</Layout.Stack>
{#if action === 'button'}
<Layout.Stack direction="row" justifyContent="flex-end">
<PinkButton.Button
size="xs"
variant="secondary"
on:click={() => dispatch('connect', repo)}>
Connect
</PinkButton.Button>
color="--fgcolor-neutral-secondary">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
</Layout.Stack>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{#if action === 'button'}
<Layout.Stack
direction="row"
justifyContent="flex-end">
<PinkButton.Button
size="xs"
variant="secondary"
on:click={() => dispatch('connect', repo)}>
Connect
</PinkButton.Button>
</Layout.Stack>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
</Paginator>
{:else}
<EmptySearch hidePages bind:search target="repositories">
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
{#if search}
<Button secondary on:click={() => (search = '')}>
@@ -1,8 +1,10 @@
<script lang="ts">
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { iconPath } from '$lib/stores/app';
import { sdk } from '$lib/stores/sdk';
import { installation, repository } from '$lib/stores/vcs';
import { VCSDetectionType } from '@appwrite.io/console';
import { DirectoryPicker } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
@@ -16,6 +18,7 @@
export let show = false;
export let rootDir: string;
export let product: 'sites' | 'functions';
let isLoading = true;
let directories: Directory[] = [
@@ -82,17 +85,16 @@
fileCount: undefined,
thumbnailUrl: undefined
}));
// const runtime = await sdk.forProject.vcs.createRepositoryDetection(
// $installation.$id,
// $repository.id,
// path
// );
// currentDir.children.forEach((dir)=>
// {
// dir.thumbnailHtml = $iconPath(runtime.runtime, 'color')
// }
// )
const runtime = await sdk.forProject.vcs.createRepositoryDetection(
$installation.$id,
$repository.id,
VCSDetectionType.Framework, //TODO: add type: VCSDetectionType.Framework || VCSDetectionType.Runtime according to the product
path
);
//TODO: Fix runtime after passing type: runtime.framework || runtime.runtime
currentDir.children.forEach((dir) => {
dir.thumbnailUrl = $iconPath(runtime.framework, 'color');
});
directories = [...directories];
} catch (error) {
console.error(error);
+24 -30
View File
@@ -1,36 +1,30 @@
<script>
import { Card, Typography } from '@appwrite.io/pink-svelte';
export let href;
<script lang="ts">
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
export let href: string;
</script>
<Card.Link class="card" {href}>
<div class="grid-item-1">
<div class="grid-item-1-start-start">
<div class="eyebrow-heading-3"><slot name="eyebrow" /></div>
<Typography.Title size="s"><slot name="title" /></Typography.Title>
<div class="u-padding-block-start-4"><slot name="subtitle" /></div>
</div>
<div class="grid-item-1-start-end">
<slot name="status" />
</div>
<div class="grid-item-1-end-start">
<div class="u-flex u-gap-16 u-flex-wrap">
<Layout.Stack height="calc(182 / 16 * 1rem)" justifyContent="space-between">
<Layout.Stack direction="row">
<Layout.Stack gap="xs">
<Typography.Caption variant="400" color="--fgcolor-neutral-tertiary"
><slot name="eyebrow" /></Typography.Caption>
<Typography.Title size="s" truncate><slot name="title" /></Typography.Title>
<div>
<slot name="subtitle" />
</div>
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center">
<slot name="status" />
</Layout.Stack>
</Layout.Stack>
<Layout.Stack direction="row">
<Layout.Stack direction="row">
<slot />
</div>
</div>
<div class="grid-item-1-end-end">
<ul class="icons u-flex u-gap-8">
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center">
<slot name="icons" />
</ul>
</div>
</div>
</Layout.Stack>
</Layout.Stack>
</Layout.Stack>
</Card.Link>
<style>
/* TODO: remove this when ui library is updated*/
.grid-item-1 {
min-block-size: calc(182 / 16 * 1rem);
}
</style>
+5 -4
View File
@@ -6,7 +6,7 @@
type Props = ComponentProps<Selector>;
export let group: string;
export let value: string | number | boolean;
export let value: string;
export let tooltipText: string = null;
export let tooltipShow = false;
@@ -15,6 +15,7 @@
export let imageRadius: Props['imageRadius'] = 'xxs';
export let padding: Props['padding'] = 's';
export let variant: Props['variant'] = 'primary';
export let name: Props['name'] = undefined;
//temporarily unefined
export let title: Props['title'] = undefined;
export let disabled = false;
@@ -27,6 +28,7 @@
<Tooltip disabled={!tooltipText || !tooltipShow}>
<Card.Selector
{name}
{src}
{alt}
{padding}
@@ -38,10 +40,9 @@
title={title ?? slotTitle?.innerText}
bind:group>
{#if $$slots.default}
<p>
<slot />
</p>
<slot />
{/if}
<slot name="action" slot="action" />
</Card.Selector>
<span slot="tooltip">{tooltipText}</span>
</Tooltip>
+1 -1
View File
@@ -36,6 +36,6 @@
<Layout.Stack direction="row" alignItems="center" inline>
<InputSelect id="rows" {options} bind:value={limit} on:change={limitChange} />
<p class="text" style:white-space="nowrap">
{name} per page. Total results: {sum >= 5000 ? `${sum}+` : sum}
{name} per page. Total: {sum >= 5000 ? `${sum}+` : sum}
</p>
</Layout.Stack>
+2
View File
@@ -0,0 +1,2 @@
export { default as Menu } from './menu.svelte';
export { default as SubMenu } from './subMenu.svelte';
+54
View File
@@ -0,0 +1,54 @@
<script lang="ts">
import { Card } from '@appwrite.io/pink-svelte';
import { createMenubar, melt } from '@melt-ui/svelte';
const {
elements: { menubar },
builders: { createMenu }
} = createMenubar();
const {
elements: { trigger: trigger, menu: menu, separator: separator },
states: { open }
} = createMenu();
function toggle() {
open.update((state) => !state);
}
</script>
<div use:melt={$menubar}>
<div use:melt={$trigger}>
<slot />
</div>
<div class="menu" use:melt={$menu}>
<Card.Base padding="xxxs">
{#if $$slots.start}
<slot name="start" />
<div class="separator" use:melt={$separator} />
{/if}
<slot name="menu" {toggle} />
{#if $$slots.end}
<div class="separator" use:melt={$separator} />
<slot name="end" {toggle} />
{/if}
</Card.Base>
</div>
</div>
<style>
.menu {
min-width: 244px;
width: max-content;
z-index: 20;
}
.separator {
height: 1px;
margin-block: 2px;
margin-inline-start: calc(var(--base-4) * -1);
width: calc(100% + var(--base-8));
background-color: var(--border-neutral);
}
</style>
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
import { Card } from '@appwrite.io/pink-svelte';
import { createMenubar, melt } from '@melt-ui/svelte';
const {
builders: { createMenu }
} = createMenubar();
const {
elements: { separator: separator },
builders: { createSubmenu: createSubmenu }
} = createMenu();
const {
elements: { subMenu: subMenu, subTrigger: subTrigger }
} = createSubmenu();
</script>
<div use:melt={$subTrigger}>
<slot />
</div>
<div class="subMenu" use:melt={$subMenu}>
<Card.Base padding="xxxs">
{#if $$slots.start}
<slot name="start" />
<div class="separator" use:melt={$separator} />
{/if}
<slot name="menu" />
{#if $$slots.end}
<div class="separator" use:melt={$separator} />
<slot name="end" />
{/if}
</Card.Base>
</div>
<style>
.subMenu {
min-width: 244px;
margin-inline: -4px;
margin-block: -4px;
}
</style>
@@ -10,10 +10,10 @@
}
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountCreate);
trackEvent(Submit.AccountLogin, { mfa_used: true });
} catch (error) {
inputDigitFields?.clearInputsAndRefocus();
trackError(error, Submit.AccountCreate);
trackError(error, Submit.AccountLogin);
throw error;
}
}
@@ -33,7 +33,6 @@
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;
+6 -8
View File
@@ -1,14 +1,12 @@
<script lang="ts">
import { Alert } from '$lib/components';
import { Form } from '$lib/elements/forms';
import { disableCommands } from '$lib/commandCenter';
import { beforeNavigate } from '$app/navigation';
import { Layout, Modal } from '@appwrite.io/pink-svelte';
import { trackEvent } from '$lib/actions/analytics';
import { Alert, Layout, Modal } from '@appwrite.io/pink-svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
export let show = false;
export let error: string = null;
export let closable = true;
export let dismissible = true;
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
return;
@@ -29,7 +27,7 @@
event.preventDefault();
if (show) {
formComponent.triggerSubmit();
trackEvent('click_submit_form', { from: 'enter' });
trackEvent(Click.SubmitFormClick, { from: 'enter' });
}
}
}
@@ -48,14 +46,14 @@
<slot slot="description" name="description" />
{#if error}
<div bind:this={alert}>
<Alert
<Alert.Inline
dismissible
type="warning"
status="warning"
on:dismiss={() => {
error = null;
}}>
{error}
</Alert>
</Alert.Inline>
</div>
{/if}
<slot />
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { ModalWrapper } from '$lib/components';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
export let show = false;
export let title = '';
@@ -28,7 +28,7 @@
aria-label="Close Modal"
title="Close Modal"
on:click={() =>
trackEvent('click_close_modal', {
trackEvent(Click.ModalCloseClick, {
from: 'button'
})}
on:click={close}>
+3 -3
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { disableCommands } from '$lib/commandCenter';
export let show = false;
@@ -23,7 +23,7 @@
function handleBLur(event: MouseEvent) {
if (event.target === dialog) {
trackEvent('click_close_modal', {
trackEvent(Click.ModalCloseClick, {
from: 'backdrop'
});
closeModal();
@@ -50,7 +50,7 @@
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
trackEvent('click_close_modal', {
trackEvent(Click.ModalCloseClick, {
from: 'escape'
});
closeModal();
+6 -5
View File
@@ -44,7 +44,7 @@
import { isTabletViewport } from '$lib/stores/viewport';
import { isCloud } from '$lib/system.js';
import { user } from '$lib/stores/user';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
let showSupport = false;
@@ -80,6 +80,7 @@
}
function toggleFeedback() {
trackEvent(Click.FeedbackSubmitClick);
feedback.toggleFeedback();
if ($feedback.notification) {
feedback.toggleNotification();
@@ -138,7 +139,7 @@
size="s"
variant="primary"
on:click={() => {
trackEvent('click_organization_upgrade', {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'top_nav'
});
@@ -153,7 +154,7 @@
variant="compact"
on:click={() => {
toggleFeedback();
trackEvent('click_menu_feedback', { source: 'top_nav' });
trackEvent(Click.FeedbackSubmitClick, { source: 'top_nav' });
}}
>Feedback
</Button.Button>
@@ -173,7 +174,7 @@
type="button"
on:click={() => {
showSupport = !showSupport;
trackEvent('click_menu_support', { source: 'top_nav' });
trackEvent(Click.SupportOpenClick, { source: 'top_nav' });
}}>
Support
</Button.Button>
@@ -199,7 +200,7 @@
showAccountMenu = !showAccountMenu;
shouldAnimateThemeToggle = false;
if (showAccountMenu) {
trackEvent('click_menu_dropdown');
trackEvent(Click.MenuDropDownClick);
}
}}>
<div style:user-select="none">
+56 -59
View File
@@ -7,8 +7,8 @@
import Actions from './actions.svelte';
import type { Permission } from './permissions.svelte';
import Row from './row.svelte';
import { Icon, Layout, Table } from '@appwrite.io/pink-svelte';
import { IconPlus, IconTable, IconX } from '@appwrite.io/pink-icons-svelte';
import { Card, Icon, Layout, Table } from '@appwrite.io/pink-svelte';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
export let roles: string[] = [];
@@ -16,7 +16,6 @@
let showTeam = false;
let showLabel = false;
let showCustom = false;
let showDropdown = false;
const groups = writable<Map<string, Permission>>(new Map());
@@ -48,8 +47,6 @@
return n;
});
showDropdown = false;
}
function deleteRole(role: string): void {
@@ -80,59 +77,59 @@
</script>
{#if [...$groups.keys()]?.length}
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Role</Table.Header.Cell>
<Table.Header.Cell width="40px" />
</svelte:fragment>
{#each [...$groups.keys()].sort(sortRoles) as role}
<Table.Row>
<Table.Cell>
<Row {role} />
</Table.Cell>
<Table.Cell>
<Layout.Stack justifyContent="flex-end">
<Button icon on:click={() => deleteRole(role)}>
<Icon icon={IconX} size="s" />
</Button>
</Layout.Stack>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
<Actions
bind:showLabel
bind:showCustom
bind:showTeam
bind:showUser
{groups}
on:create={create}
let:toggle>
<Button text on:click={toggle}>
<Icon icon={IconPlus} slot="start" size="s" />
Add role
</Button>
</Actions>
<Layout.Stack>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Role</Table.Header.Cell>
<Table.Header.Cell width="40px" />
</svelte:fragment>
{#each [...$groups.keys()].sort(sortRoles) as role}
<Table.Row>
<Table.Cell>
<Row {role} />
</Table.Cell>
<Table.Cell>
<Layout.Stack justifyContent="flex-end">
<Button compact icon on:click={() => deleteRole(role)}>
<Icon icon={IconX} size="s" />
</Button>
</Layout.Stack>
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
<Actions
bind:showLabel
bind:showCustom
bind:showTeam
bind:showUser
{groups}
on:create={create}
let:toggle>
<div>
<Button compact on:click={toggle}>
<Icon icon={IconPlus} slot="start" size="s" />
Add role
</Button>
</div>
</Actions>
</Layout.Stack>
{:else}
<article class="card u-grid u-cross-center u-width-full-line dashed">
<div class="u-flex u-cross-center u-flex-vertical u-main-center u-flex">
<div class="common-section">
<Actions
bind:showLabel
bind:showCustom
bind:showTeam
bind:showUser
{groups}
on:create={create}
let:toggle>
<Button secondary icon on:click={toggle}>
<Icon icon={IconPlus} size="s" />
</Button>
</Actions>
</div>
<div class="common-section">
<span class="text"> Add a role </span>
</div>
</div>
</article>
<Card.Base>
<Layout.Stack justifyContent="center" alignItems="center" gap="m">
<Actions
bind:showLabel
bind:showCustom
bind:showTeam
bind:showUser
{groups}
on:create={create}
let:toggle>
<Button secondary icon on:click={toggle}>
<Icon icon={IconPlus} size="s" />
</Button>
</Actions>
<span class="text">Add a role </span>
</Layout.Stack>
</Card.Base>
{/if}
+84 -131
View File
@@ -1,17 +1,24 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { tick } from 'svelte';
import { AvatarInitials } from '../';
import Output from '../output.svelte';
import {
Button,
Divider,
Icon,
Layout,
Link,
Popover,
Spinner,
Typography
} from '@appwrite.io/pink-svelte';
import Avatar from '../avatar.svelte';
import { IconAnonymous, IconExternalLink, IconMinusSm } from '@appwrite.io/pink-icons-svelte';
import { base } from '$app/paths';
import { page } from '$app/stores';
export let role: string;
let content: HTMLDivElement;
let data = null;
let isFetching = false;
async function getData(
permission: string
): Promise<
@@ -20,139 +27,85 @@
const role = permission.split(':')[0];
const id = permission.split(':')[1].split('/')[0];
if (role === 'user') {
const user = await sdk.forProject.users.get(id);
return user;
return await sdk.forProject.users.get(id);
}
if (role === 'team') {
const team = await sdk.forProject.teams.get(id);
return team;
return await sdk.forProject.teams.get(id);
}
}
</script>
<div class="u-flex u-cross-center u-gap-8 tippy-user">
<div>
{#if role === 'users'}
<div>Users</div>
{:else if role === 'guests'}
<div>Guests</div>
{:else if role === 'any'}
<div>Any</div>
{:else}
<div
class="u-trim-1"
use:tooltip={{
interactive: true,
allowHTML: true,
onShow(instance) {
if (isFetching || data) {
return;
}
getData(role)
.then((n) => {
data = n;
})
.finally(() => {
tick().then(() => {
instance.setContent(content);
});
});
}
}}>
{role}
</div>
<div class="u-hide">
<div bind:this={content}>
{#if data}
{@const isUser = role.startsWith('user')}
{@const isTeam = role.startsWith('team')}
{@const isAnonymous = !data.email && !data.phone && isUser}
<div class="user-profile">
{#if role === 'users'}
<div>Users</div>
{:else if role === 'guests'}
<div>Guests</div>
{:else if role === 'any'}
<div>Any</div>
{:else}
<Popover let:toggle placement="bottom-start">
<Link.Button on:click={toggle}>{role}</Link.Button>
<div let:showing slot="tooltip" style:width="200px">
{#key showing}
{#await getData(role)}
<Layout.Stack alignItems="center">
<Spinner />
</Layout.Stack>
{:then data}
{@const isUser = role.startsWith('user')}
{@const isTeam = role.startsWith('team')}
{@const isAnonymous = !data.email && !data.phone && !data.name && isUser}
<Layout.Stack>
<Layout.Stack direction="row" gap="s" alignItems="center">
{#if isAnonymous}
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true" />
</div>
<Avatar alt="avatar" size="xs">
<Icon icon={IconAnonymous} size="s" />
</Avatar>
{:else if data.name}
<AvatarInitials name={data.name} size="s" />
<AvatarInitials name={data.name} size="xs" />
{:else}
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true" />
</div>
<Avatar alt="avatar" size="xs">
<Icon icon={IconMinusSm} size="s" />
</Avatar>
{/if}
<span class="user-profile-info is-only-desktop">
<span class="name">
{data.name ?? data?.email ?? data?.phone ?? '-'}
</span>
<Output value={data.$id}>{role}</Output>
</span>
{#if (isUser && (data?.email || data?.phone)) || isTeam}
<span class="user-profile-sep" />
<Typography.Text truncate color="--fgcolor-neutral-primary">
{data.name ?? data?.email ?? data?.phone ?? '-'}
</Typography.Text>
</Layout.Stack>
<span class="user-profile-empty-column" />
<span class="user-profile-info is-only-desktop">
{#if isUser}
<div class="u-grid u-gap-4">
{#if data?.email}
<p class="text u-x-small">Email: {data?.email}</p>
{/if}
{#if data?.phone}
<p class="text u-x-small">Phone: {data?.phone}</p>
{/if}
</div>
{:else if isTeam}
<p class="text u-x-small">Members: {data?.total}</p>
{/if}
</span>
<Divider />
{#if isUser}
{#if data?.email}
<Typography.Text truncate>Email: {data?.email}</Typography.Text>
{/if}
</div>
{:else}
Not found.
{/if}
</div>
</div>
{/if}
</div>
</div>
<!-- svelte-ignore css-unused-selector -->
<style lang="scss" global>
.tippy-user .tippy-box {
--p-drop-bg-color: var(--color-neutral-105);
--p-drop-border-color: var(--color-neutral-85);
inset-inline-start: -0.625rem;
inset-block-end: calc(100% + 0.625rem);
background-color: hsl(var(--p-drop-bg-color));
border: solid 0.0625rem hsl(var(--p-drop-border-color));
border-radius: var(--border-radius-small);
box-shadow: var(--shadow-small);
font-size: var(--font-size-0);
color: hsl(var(--p-body-text-color));
max-inline-size: 32.5rem;
margin-inline: auto;
line-height: 1.5;
body.theme-light & {
--p-drop-bg-color: var(--color-neutral-0);
--p-drop-border-color: var(--color-neutral-10);
}
.tippy-content {
padding: 1rem;
}
&[data-placement^='top'] > .tippy-arrow::before {
border-top-color: hsl(var(--p-drop-bg-color));
}
&[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: hsl(var(--p-drop-bg-color));
}
&[data-placement^='left'] > .tippy-arrow::before {
border-left-color: hsl(var(--p-drop-bg-color));
}
&[data-placement^='right'] > .tippy-arrow::before {
border-right-color: hsl(var(--p-drop-bg-color));
}
}
</style>
{#if data?.phone}
<Typography.Text truncate>Phone: {data?.phone}</Typography.Text>
{/if}
<div>
<Button.Anchor
href={`${base}/project-${$page.params.project}/auth/user-${data?.$id}`}
size="xs"
target="_blank"
variant="secondary">
View user
<Icon slot="end" icon={IconExternalLink} size="s" />
</Button.Anchor>
</div>
{:else if isTeam}
<Typography.Text>Members: {data?.total}</Typography.Text>
<div>
<Button.Anchor
href={`${base}/project-${$page.params.project}/auth/teams/team-${data?.$id}`}
size="s"
target="_blank"
variant="secondary">
View team
<Icon slot="end" icon={IconExternalLink} size="s" />
</Button.Anchor>
</div>
{/if}
</Layout.Stack>
{/await}
{/key}
</div>
</Popover>
{/if}
+3 -12
View File
@@ -70,7 +70,7 @@
}
</script>
<Modal title="Select teams" bind:show onSubmit={create} on:close={reset} size="big">
<Modal title="Select teams" bind:show onSubmit={create} on:close={reset}>
<Typography.Text
>Grant access to any member of a specific team. To grant access to team members with
specific roles, you will need to set a <Link.Button on:click={() => dispatch('custom')}
@@ -93,23 +93,14 @@
<Layout.Stack direction="row" alignItems="center" gap="s">
<AvatarInitials size="xs" name={team.name} />
<Layout.Stack gap="none">
<Typography.Caption variant="400">Text</Typography.Caption>
<Typography.Caption variant="400">{team.name}</Typography.Caption>
<Typography.Caption
variant="400"
color="--fgcolor-neutral-tertiary">
Secondary Text
{team.$id}
</Typography.Caption>
</Layout.Stack>
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" gap="s">
<AvatarInitials size="xs" name={team.name} />
<span>
{team.name}
</span>
<span>
{team.$id}
</span>
</Layout.Stack>
</Table.Cell>
</Table.Button>
{/each}
+1 -7
View File
@@ -17,13 +17,7 @@
Table,
Typography
} from '@appwrite.io/pink-svelte';
import {
IconAnonymous,
IconChartSquareBar,
IconCheck,
IconMinus,
IconMinusSm
} from '@appwrite.io/pink-icons-svelte';
import { IconAnonymous, IconMinusSm } from '@appwrite.io/pink-icons-svelte';
export let show: boolean;
export let groups: Writable<Map<string, Permission>>;
+16 -12
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { ProgressBar, type ProgressbarData } from '$lib/components/progressbar';
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
export let currentValue: string | undefined = undefined;
export let currentUnit: string | undefined = undefined;
@@ -16,18 +17,21 @@
<section class="progress-bar">
{#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined && maxValue !== undefined}
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<p>
<span class="heading-level-4">{currentValue}</span>
<span class="body-text-1 u-bold">{currentUnit}</span>
</p>
<p class="heading-level-4">{progress}%</p>
</div>
<p class="body-text-2">
{maxValue}
{maxUnit ? maxUnit : ''}
</p>
<Layout.Stack direction="row" alignItems="center">
<Layout.Stack gap="s" direction="row" alignItems="baseline">
<Typography.Title>
{currentValue}
</Typography.Title>
<Typography.Text>{currentUnit}</Typography.Text>
<Typography.Text color="--fgcolor-neutral-tertiary">
{maxValue}
{maxUnit ? maxUnit : ''}
</Typography.Text>
</Layout.Stack>
<div>
<Badge variant="secondary" size="xs" content={`${progress}%`} />
</div>
</Layout.Stack>
</div>
{/if}
{#if showBar && progressBarData.length > 0}
@@ -29,7 +29,7 @@
style:width={`${(item.size / maxSize) * 100}%`}>
</div>
<div slot="tooltip">
<span class="u-bold">${item.tooltip.title}</span> ${item.tooltip.label}
<span class="u-bold">{item.tooltip.title}</span> ${item.tooltip.label}
</div>
</Tooltip>
{/each}
+6 -5
View File
@@ -34,7 +34,7 @@
import MobileFeedbackModal from '$routes/(console)/wizard/feedback/mobileFeedbackModal.svelte';
import { getSidebarState, updateSidebarState } from '$lib/helpers/sidebar';
import { isTabletViewport } from '$lib/stores/viewport';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
type $$Props = HTMLElement & {
state?: 'closed' | 'open' | 'icons';
@@ -58,6 +58,7 @@
export let subNavigation = undefined;
function toggleFeedback() {
trackEvent(Click.FeedbackSubmitClick);
feedback.toggleFeedback();
if ($feedback.notification) {
feedback.toggleNotification();
@@ -139,7 +140,7 @@
class="link"
class:active={pathname.includes('overview')}
on:click={() => {
trackEvent('click_menu_overview');
trackEvent(Click.MenuOverviewClick);
sideBarIsOpen = false;
}}
><span class="link-icon"
@@ -243,7 +244,7 @@
size="s"
on:click={() => {
toggleFeedback();
trackEvent('click_menu_feedback', { source: 'side_nav' });
trackEvent(Click.FeedbackSubmitClick, { source: 'side_nav' });
}}
>Feedback
</Button.Button>
@@ -258,7 +259,7 @@
size="s"
on:click={() => {
$showSupportModal = true;
trackEvent('click_menu_support', { source: 'side_nav' });
trackEvent(Click.SupportOpenClick, { source: 'side_nav' });
}}>
<span>Support</span>
@@ -318,7 +319,7 @@
size="s"
on:click={() => {
$showSupportModal = true;
trackEvent('click_menu_support', { source: 'side_nav' });
trackEvent(Click.SupportOpenClick, { source: 'side_nav' });
}}>
<span>Support</span>
+3 -3
View File
@@ -3,7 +3,7 @@
import { wizard } from '$lib/stores/wizard';
import SupportWizard from '$routes/(console)/supportWizard.svelte';
import { isSupportOnline, showSupportModal } from '$routes/(console)/wizard/support/store';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { localeShortTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
import { upgradeURL } from '$lib/stores/billing';
import { Card } from '$lib/components/index';
@@ -82,7 +82,7 @@
<Button
href={$upgradeURL}
on:click={() => {
trackEvent('click_organization_upgrade', {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'support_menu'
});
@@ -119,7 +119,7 @@
secondary
class="secondary-button u-flex u-cross-center u-gap-6"
on:click={() => {
trackEvent('click_organization_upgrade', {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'support_menu'
});
+9 -1
View File
@@ -4,5 +4,13 @@
</script>
{#if $uploader?.isOpen}
<Upload.Box files={$uploader.files} on:close={() => uploader.close()} />
<Upload.Box
files={$uploader.files.map((file) => {
return {
name: file.name,
size: file.size,
status: file.status
};
})}
on:close={() => uploader.close()} />
{/if}
+1 -1
View File
@@ -16,7 +16,7 @@
<Typography.Text size="s" truncate color="--fgcolor-neutral-primary"
>{value}</Typography.Text>
{:else}
<Skeleton variant="line" width={100} height={19.5} />
<Skeleton variant="line" width="100%" height={19.5} />
{/if}
</slot>
</Layout.Stack>
+9 -8
View File
@@ -23,7 +23,6 @@
export let hideView = false;
export let hideColumns = false;
export let allowNoColumns = false;
export let fullWidthMobile = false;
onMount(async () => {
if (isCustomCollection) {
@@ -76,12 +75,14 @@
});
}
$: selectedColumnsNumber = $columns.reduce((acc, column) => {
if (column.show) {
acc++;
}
return acc;
}, 0);
$: selectedColumnsNumber = $columns
.filter((c) => !c.hide)
.reduce((acc, column) => {
if (column.show) {
acc++;
}
return acc;
}, 0);
</script>
{#if !hideColumns && view === View.Table}
@@ -97,7 +98,7 @@
<svelte:fragment slot="tooltip">
<ActionMenu.Root>
<Layout.Stack>
{#each $columns as column}
{#each $columns.filter((c) => !c.hide) as column}
<InputCheckbox
id={column.id}
label={column.title}
+13
View File
@@ -8,6 +8,7 @@ export enum Dependencies {
CREDIT = 'dependency:credit',
INVOICES = 'dependency:invoices',
ADDRESS = 'dependency:address',
UPGRADE_PLAN = 'dependency:upgrade_plan',
PAYMENT_METHODS = 'dependency:paymentMethods',
ORGANIZATION = 'dependency:organization',
MEMBERS = 'dependency:members',
@@ -376,6 +377,18 @@ export const scopes: {
description: "Access to create, update, and delete your project's sites and deployments",
category: 'Sites',
icon: 'globe'
},
{
scope: 'log.read',
description: "Access to read your sites's logs",
category: 'Sites',
icon: 'globe'
},
{
scope: 'log.write',
description: "Access to execute your project's sites",
category: 'Sites',
icon: 'globe'
}
];
@@ -11,6 +11,7 @@
export let disabled = false;
export let tooltip: string = null;
export let fullWidth = false;
export let description = '';
let element: HTMLInputElement;
let error: string;
@@ -36,6 +37,7 @@
{id}
{disabled}
{required}
{description}
bind:checked={value}
on:change
on:invalid={handleInvalid} />
@@ -43,6 +45,7 @@
<Selector.Checkbox
{id}
{disabled}
{description}
size="s"
{required}
bind:checked={value}
+33 -67
View File
@@ -1,95 +1,61 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import { Input } from '@appwrite.io/pink-svelte';
export let label: string;
export let showLabel = true;
export let optionalText: string | undefined = undefined;
export let label: string = undefined;
export let id: string;
export let name: string = id;
export let helper: string = undefined;
export let value = '';
export let placeholder = '';
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;
export let autocomplete = false;
export let fullWidth = false;
export let step: number | 'any' = 0.001;
export let min: string = undefined;
export let max: string = undefined;
let element: HTMLInputElement;
let error: string;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
function handleInvalid(event: Event) {
event.preventDefault();
if (element.validity.valueMissing) {
if (event.currentTarget.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
}
let prevValue = '';
function handleNullChange(e: CustomEvent<boolean>) {
const isNull = e.detail;
if (isNull) {
prevValue = value;
value = null;
} else {
value = prevValue;
}
error = event.currentTarget.validationMessage;
}
$: if (value) {
error = null;
}
$: isNullable = nullable && !required;
</script>
<Label {required} {optionalText} hide={!showLabel} for={id}>
<Input.DateTime
{id}
{name}
{placeholder}
{disabled}
{required}
{label}
</Label>
<div class="input-text-wrapper">
<input
{id}
{disabled}
{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}
<ul
class="buttons-list u-cross-center u-gap-8 u-position-absolute u-inset-block-start-8 u-inset-block-end-8 u-inset-inline-end-12">
<li class="buttons-list-item">
<NullCheckbox checked={value === null} on:change={handleNullChange} />
</li>
</ul>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
{/if}
{step}
{nullable}
{readonly}
{min}
{max}
type="date"
autofocus={autofocus || undefined}
autocomplete={autocomplete ? 'on' : 'off'}
helper={error || helper}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
on:input
bind:value>
<slot name="start" slot="start" />
<slot name="info" slot="info" />
<slot name="end" slot="end" />
</Input.DateTime>
+1 -6
View File
@@ -6,11 +6,6 @@
export let required = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let fullWidth = false;
export let autoSubmit = true;
$: console.log(value);
</script>
<Input.OTP {length} bind:value {required} {disabled} {readonly} {autofocus} size="s" {fullWidth} />
<Input.OTP {length} bind:value {required} {disabled} {readonly} size="s" />
@@ -4,6 +4,7 @@
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import type { Models } from '@appwrite.io/console';
import { Label } from '.';
import { Upload } from '@appwrite.io/pink-svelte';
export let label: string = null;
export let value: Models.File = null;
+3 -1
View File
@@ -58,4 +58,6 @@
on:invalid={handleInvalid}
on:input
on:change
bind:value />
bind:value>
<slot name="info" slot="info" />
</Input.Select>
@@ -144,15 +144,3 @@
<Helper class="u-position-relative" type="warning">{error}</Helper>
{/if}
</div> -->
<style>
.form-item :global(.drop) {
translate: 0 4px;
}
.form-item :global(.drop-section) {
width: 100%;
margin-inline: initial;
max-inline-size: initial;
}
</style>
+34 -43
View File
@@ -1,39 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Helper, Label } from '.';
import { Input } from '@appwrite.io/pink-svelte';
export let label: string;
export let showLabel = true;
export let optionalText: string | undefined = undefined;
export let label: string = undefined;
export let id: string;
export let name: string = id;
export let helper: string = undefined;
export let value = '';
export let placeholder = '';
export let required = false;
export let min: string | number | undefined = undefined;
export let max: string | number | undefined = undefined;
export let nullable = false;
export let disabled = false;
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
export let step: number | 'any' = 60;
export let step: number | 'any' = 0.001;
export let min: string = undefined;
export let max: string = undefined;
let element: HTMLInputElement;
let error: string;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
function handleInvalid(event: Event) {
event.preventDefault();
if (element.validity.valueMissing) {
if (event.currentTarget.validity.valueMissing) {
error = 'This field is required';
return;
}
error = element.validationMessage;
error = event.currentTarget.validationMessage;
}
$: if (value) {
@@ -41,30 +35,27 @@
}
</script>
<Label {required} {optionalText} hide={!showLabel} for={id}>
<Input.DateTime
{id}
{name}
{placeholder}
{disabled}
{required}
{label}
</Label>
<div class="input-text-wrapper" style="--amount-of-buttons:1; --button-size: 1rem">
<input
{id}
{disabled}
{readonly}
{required}
{min}
{max}
{step}
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}
{step}
{nullable}
{readonly}
{min}
{max}
type="time"
autofocus={autofocus || undefined}
autocomplete={autocomplete ? 'on' : 'off'}
helper={error || helper}
state={error ? 'error' : 'default'}
on:invalid={handleInvalid}
on:input
bind:value>
<slot name="start" slot="start" />
<slot name="info" slot="info" />
<slot name="end" slot="end" />
</Input.DateTime>
+3 -1
View File
@@ -13,6 +13,7 @@
export let variant: Props['variant'] = 'default';
export let size: Props['size'] = 'm';
export let external = false;
export let icon = false;
function track() {
if (!event) {
@@ -33,6 +34,7 @@
on:click={track}
{href}
{disabled}
{icon}
{variant}
{size}
target={external ? '_blank' : ''}
@@ -40,7 +42,7 @@
<slot />
</Link.Anchor>
{:else}
<Link.Button on:click on:mousedown on:click={track} {type} {disabled} {variant} {size}>
<Link.Button on:click on:mousedown on:click={track} {type} {disabled} {variant} {size} {icon}>
<slot />
</Link.Button>
{/if}
@@ -3,7 +3,6 @@
import InputCheckbox from './forms/inputCheckbox.svelte';
export let value = false;
export let padding: number | null = null;
</script>
<ActionMenu.Item.Button on:click>
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { getServiceLimit, upgradeURL, type PlanServices } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { Button } from '../forms';
@@ -41,7 +41,7 @@
secondary
href={$upgradeURL}
on:click={() =>
trackEvent('click_organization_upgrade', {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: event ?? 'table_row_limit_reached'
})}>Upgrade plan</Button>
+11
View File
@@ -86,6 +86,17 @@ export async function gzipUpload(files: FileList) {
return uploadFile;
}
export function removeFile(file: File, files: FileList) {
const filteredFiles = Array.from(files).filter((f) => f.name !== file.name);
const dataTransfer = new DataTransfer();
filteredFiles.forEach((file) => {
dataTransfer.items.add(file);
});
return dataTransfer.files;
}
export const defaultIgnore = `
### Node ###
# Logs
+8
View File
@@ -0,0 +1,8 @@
import { isValueOfStringEnum } from '$lib/helpers/types';
import { Flag } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
export function getFlagUrl(countryCode: string) {
if (!isValueOfStringEnum(Flag, countryCode)) return '';
return sdk.forProject.avatars.getFlag(countryCode, 22, 15, 100)?.toString();
}
+2 -2
View File
@@ -5,10 +5,10 @@ import { Submit, trackEvent } from '$lib/actions/analytics';
import { base } from '$app/paths';
import { uploader } from '$lib/stores/uploader';
export async function logout() {
export async function logout(redirect: boolean = true) {
await sdk.forConsole.account.deleteSession('current');
await invalidate(Dependencies.ACCOUNT);
uploader.reset();
trackEvent(Submit.AccountLogout);
await goto(`${base}/login`);
if (redirect) await goto(`${base}/login`);
}
+4
View File
@@ -39,6 +39,10 @@ export type Column = {
array?: boolean;
format?: string;
elements?: string[] | { value: string | number; label: string }[];
/**
* Set to true to hide this column by default
*/
hide?: boolean;
};
export function isValueOfStringEnum<T extends Record<string, string>>(
+2 -2
View File
@@ -6,12 +6,12 @@
</script>
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
export let breadcrumbs: Breadcrumb[];
function track() {
trackEvent('click_breadcrumb');
trackEvent(Click.BreadcrumbClick);
}
</script>
+3 -1
View File
@@ -14,9 +14,10 @@
} plan`;
export let disabled: boolean;
export let buttonText: string;
export let buttonMethod: () => void | Promise<void>;
export let buttonMethod: () => void | Promise<void> = () => {};
export let buttonHref: string = null;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let buttonEventData: Record<string, unknown> = {};
export let icon = 'plus';
export let showIcon = true;
export let buttonType: 'primary' | 'secondary' | 'text' = 'primary';
@@ -29,6 +30,7 @@
secondary={buttonType === 'secondary'}
on:click={buttonMethod}
event={buttonEvent}
eventData={buttonEventData}
{disabled}
href={buttonHref}>
{#if showIcon}
+2
View File
@@ -29,6 +29,7 @@
export let buttonMethod: () => void = null;
export let buttonHref: string = null;
export let buttonEvent: string = buttonText?.toLocaleLowerCase();
export let buttonEventData: Record<string, unknown> = {};
export let buttonDisabled = false;
let showDropdown = false;
@@ -172,6 +173,7 @@
disabled={isButtonDisabled}
{buttonText}
{buttonEvent}
{buttonEventData}
{buttonMethod}
{buttonHref} />
{/if}
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import { Layout, Typography, Input, Tag, Icon, Button } from '@appwrite.io/pink-svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { CustomId } from '$lib/components/index.js';
import type { Region } from '$lib/sdk/billing';
import { getFlagUrl } from '$lib/helpers/flag';
import { isCloud } from '$lib/system.js';
export let projectName: string;
export let id: string;
export let regions: Array<Region> = [];
export let region: string;
export let showTitle = true;
export let createProject: () => Promise<void>;
let showCustomId = false;
function getRegions() {
return regions
.filter((region) => region.$id !== 'default')
.sort((regionA, regionB) => {
if (regionA.disabled && !regionB.disabled) {
return 1;
}
return regionA.name > regionB.name ? 1 : -1;
})
.map((region) => {
return {
label: region.name,
value: region.$id,
leadingHtml: `<img src='${getFlagUrl(region.flag)}' alt='Region flag'/>`,
disabled: region.disabled
};
});
}
</script>
<svelte:head>
{#each regions as region}
<link rel="preload" as="image" href={getFlagUrl(region.flag)} />
{/each}
</svelte:head>
<form>
<Layout.Stack direction="column" gap="xxl">
{#if showTitle}
<Typography.Title size="l">Create your project</Typography.Title>
{/if}
<Layout.Stack direction="column" gap="xxl">
<Layout.Stack direction="column" gap="xxl">
<Layout.Stack direction="column" gap="s">
<Input.Text
label="Name"
placeholder="Project name"
required
bind:value={projectName} />
{#if !showCustomId}
<div>
<Tag
size="s"
on:click={() => {
showCustomId = true;
}}><Icon icon={IconPencil} /> Project ID</Tag>
</div>
{/if}
<CustomId
bind:show={showCustomId}
name="Project"
isProject
bind:id
fullWidth={true} />
</Layout.Stack>
{#if isCloud && regions.length > 0}
<Layout.Stack gap="xs"
><Input.Select
bind:value={region}
placeholder="Select a region"
options={getRegions()}
label="Region" />
<Typography.Text>Region cannot be changed after creation</Typography.Text>
</Layout.Stack>
{/if}
</Layout.Stack>
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="flex-end"
><Button.Button type="button" variant="primary" size="s" on:click={createProject}>
Create</Button.Button>
</Layout.Stack>
</Layout.Stack>
</form>
+4 -3
View File
@@ -6,7 +6,7 @@
import { user } from '$lib/stores/user';
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
import { Click, trackEvent } from '$lib/actions/analytics';
import { tooltip } from '$lib/actions/tooltip';
import { toggleCommandCenter } from '$lib/commandCenter/commandCenter.svelte';
import Button from '$lib/elements/forms/button.svelte';
@@ -30,6 +30,7 @@
function toggleFeedback() {
feedback.toggleFeedback();
trackEvent(Click.FeedbackSubmitClick);
if ($feedback.notification) {
feedback.toggleNotification();
feedback.addVisualization();
@@ -53,7 +54,7 @@
}
$: if (showDropdown) {
trackEvent('click_menu_dropdown');
trackEvent(Click.MenuDropDownClick);
}
const slideFade: typeof slide = (node, options) => {
@@ -97,7 +98,7 @@
disabled={$organization?.markedForDeletion}
href={$upgradeURL}
on:click={() => {
trackEvent('click_organization_upgrade', {
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'top_nav'
});
+32 -1
View File
@@ -1,9 +1,37 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { isTabletViewport } from '$lib/stores/viewport';
export let title: string;
export let type: 'info' | 'success' | 'warning' | 'error' | 'default' = 'info';
let container;
function setNavigationHeight() {
const alertHeight = container ? container.getBoundingClientRect().height : 0;
const header: HTMLHeadingElement = document.querySelector('main > header');
const sidebar: HTMLElement = document.querySelector('main > div > nav');
if (header) {
header.style.top = `${alertHeight}px`;
}
if (sidebar) {
sidebar.style.top = `${alertHeight + ($isTabletViewport ? 0 : header.getBoundingClientRect().height)}px`;
}
}
onMount(() => {
setNavigationHeight();
});
onDestroy(() => {
container = null;
setNavigationHeight();
});
</script>
<svelte:window on:resize={setNavigationHeight} />
<section
bind:this={container}
class="alert is-action is-action-and-top-sticky u-sep-block-end"
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
@@ -40,7 +68,10 @@
<style>
.alert {
padding: 1rem 1rem 0.75rem 1.5rem;
margin-block-start: 18px;
position: fixed;
top: 0;
width: 100%;
z-index: 100;
}
.alert-content {
+49 -66
View File
@@ -3,14 +3,6 @@
import { log } from '$lib/stores/logs';
import { Alert, Card, Code, Copy, Id, SvgIcon, Tab, Tabs } from '../components';
import { calculateTime } from '$lib/helpers/timeConversion';
import {
TableBody,
TableCellHead,
TableCellText,
TableHeader,
TableRow,
TableScroll
} from '$lib/elements/table';
import { beforeNavigate } from '$app/navigation';
import { Pill } from '$lib/elements';
import { isCloud } from '$lib/system';
@@ -18,7 +10,7 @@
import { organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
import { Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { Table, Tooltip, Typography } from '@appwrite.io/pink-svelte';
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
@@ -223,26 +215,22 @@
</div>
{#if selectedRequest === 'parameters'}
{#if parameters?.length}
<div class="u-margin-block-start-24">
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each parameters as param}
<TableRow>
<TableCellText title="Key">
{param.key}
</TableCellText>
<TableCellText title="Value">
{param.value}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
</div>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Name</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each parameters as param}
<Table.Row>
<Table.Cell>
{param.key}
</Table.Cell>
<Table.Cell>
{param.value}
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{/if}
<p class="text u-text-center u-padding-24">
@@ -261,26 +249,22 @@
</p>
{:else if selectedRequest === 'headers'}
{#if execution.requestHeaders.length}
<div class="u-margin-block-start-24">
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each execution.requestHeaders as header}
<TableRow>
<TableCellText title="Name">
{header.name}
</TableCellText>
<TableCellText title="Value">
{header.value}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
</div>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Name</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each execution.requestHeaders as header}
<Table.Row>
<Table.Cell>
{header.key}
</Table.Cell>
<Table.Cell>
{header.value}
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{/if}
<p class="text u-text-center u-padding-16">
@@ -379,23 +363,22 @@
{/if}
{:else if selectedResponse === 'headers'}
{#if execution.responseHeaders.length}
<TableScroll noMargin>
<TableHeader>
<TableCellHead>Name</TableCellHead>
<TableCellHead>Value</TableCellHead>
</TableHeader>
<TableBody>
{#each execution.responseHeaders as header}
<TableRow>
<TableCellText title="Name">
{header.name}
</TableCellText>
<TableCellText title="Value"
>{header.value}</TableCellText>
</TableRow>
{/each}
</TableBody>
</TableScroll>
<Table.Root>
<svelte:fragment slot="header">
<Table.Header.Cell>Name</Table.Header.Cell>
<Table.Header.Cell>Value</Table.Header.Cell>
</svelte:fragment>
{#each execution.responseHeaders as header}
<Table.Row>
<Table.Cell>
{header.key}
</Table.Cell>
<Table.Cell>
{header.value}
</Table.Cell>
</Table.Row>
{/each}
</Table.Root>
{/if}
<p class="text u-text-center u-padding-16">
{execution.responseHeaders?.length
+14 -3
View File
@@ -134,7 +134,11 @@
<svelte:window on:resize={handleResize} />
<svelte:body use:style={$bodyStyle} />
{#if $activeHeaderAlert?.show}
<svelte:component this={$activeHeaderAlert.component} />
{/if}
<main
class:has-alert={$activeHeaderAlert?.show}
class:is-open={$showSubNavigation}
class:u-hide={$wizard.show || $log.show || $wizard.cover}
class:is-fixed-layout={$activeHeaderAlert?.show}
@@ -157,9 +161,6 @@
class:icons-content={state === 'icons'}
class:no-sidebar={!showSideNavigation}>
<section class="main-content" data-test={showSideNavigation}>
{#if $activeHeaderAlert?.show}
<svelte:component this={$activeHeaderAlert.component} />
{/if}
{#if $page.data?.header}
<svelte:component this={$page.data.header} />
{/if}
@@ -247,4 +248,14 @@
grid-template-columns: auto 1fr !important;
}
}
//
//:global(main.has-alert > header) {
// top: 70px;
//}
//:global(main.has-alert > div nav) {
// @media (min-width: 1024px) {
// top: calc(48px + 70px) !important;
// height: calc(100vh - (48px + 70px)) !important;
// }
//}
</style>
+1 -1
View File
@@ -270,7 +270,7 @@
}
.tag-line {
font-family: 'Aeonik Pro';
font-family: 'Aeonik Pro', 'Inter', sans-serif;
font-size: 4rem;
font-style: normal;
font-weight: 400;

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