mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Merge branch 'feat-pink-v2' into 'add-date-tooltips'.
This commit is contained in:
+5
-5
@@ -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",
|
||||
|
||||
Generated
+26
-26
@@ -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
@@ -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%
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,6 +19,9 @@ type MenuItem = {
|
||||
trailingIcon?: ComponentType;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
closeOnClick?: boolean;
|
||||
navigatePrevious?: boolean;
|
||||
checked?: boolean;
|
||||
subMenu?: { top: SubMenu; bottom: SubMenu };
|
||||
};
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
+2
-2
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
+2
-2
@@ -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
|
||||
@@ -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}
|
||||
|
||||
@@ -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 = '')}>
|
||||
|
||||
+13
-11
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Menu } from './menu.svelte';
|
||||
export { default as SubMenu } from './subMenu.svelte';
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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'
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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>>(
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user