Merge remote-tracking branch 'origin/fix-documents-selection-state' into fix-documents-selection-state

This commit is contained in:
ItzNotABug
2024-08-27 13:33:07 +05:30
152 changed files with 12883 additions and 4105 deletions
+37 -1
View File
@@ -5,7 +5,43 @@ on:
types: [published]
jobs:
publish:
publish-cloud:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: appwrite/console-cloud
tags: |
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
publish-self-hosted:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
+13 -10
View File
@@ -49,7 +49,7 @@ git clone https://github.com/appwrite/console.git appwrite-console
Navigate to the Appwrite Console repository and install dependencies.
```bash
cd appwrite-console && npm install
cd appwrite-console && pnpm install
```
### 3. Install and run Appwrite locally
@@ -62,10 +62,13 @@ Follow the [install instructions](https://appwrite.io/docs/advanced/self-hosting
Add a `.env` file by copying the `.env.example` file as a template in the project's root directory.
> **Note**
> If you are updating from Appwrite `1.5.x`, be aware that the variables for the console in the `.env` / `.env.example` file have changed in `1.6.x`.
Finally, start a development server:
```bash
npm run dev
pnpm dev
```
> **Note**
@@ -74,7 +77,7 @@ npm run dev
### Build
```bash
npm run build
pnpm build
```
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.
@@ -82,7 +85,7 @@ npm run build
### Tests
```bash
npm test
pnpm test
```
This will run tests in the `tests/` directory.
@@ -92,13 +95,13 @@ This will run tests in the `tests/` directory.
Code should be consistently formatted everywhere. Before committing code, run the code-formatter.
```bash
npm run format
pnpm run format
```
### Linter
```bash
npm run lint
pnpm run lint
```
### Diagnostics
@@ -110,7 +113,7 @@ Diagnostic tool that checks for the following:
- TypeScript compiler errors
```bash
npm run check
pnpm run check
```
## Submit a Pull Request 🚀
@@ -173,11 +176,11 @@ $ git push origin [name_of_your_new_branch]
Before committing always make sure to run all available tools to improve the codebase:
- Formatter
- `npm run format`
- `pnpm run format`
- Tests
- `npm test`
- `pnpm test`
- Diagnostics
- `npm run check`
- `pnpm run check`
### Performance
+2 -2
View File
@@ -20,11 +20,11 @@ ADD ./static /app/static
ARG PUBLIC_CONSOLE_MODE
ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_APPWRITE_GROWTH_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
ARG PUBLIC_STRIPE_KEY
ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
ENV PUBLIC_APPWRITE_GROWTH_ENDPOINT=$PUBLIC_APPWRITE_GROWTH_ENDPOINT
ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
+1 -1
View File
@@ -6,7 +6,7 @@ services:
args:
PUBLIC_CONSOLE_MODE: ${PUBLIC_CONSOLE_MODE}
PUBLIC_APPWRITE_ENDPOINT: ${PUBLIC_APPWRITE_ENDPOINT}
PUBLIC_APPWRITE_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
PUBLIC_GROWTH_ENDPOINT: ${PUBLIC_GROWTH_ENDPOINT}
PUBLIC_STRIPE_KEY: ${PUBLIC_STRIPE_KEY}
develop:
watch:
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

After

Width:  |  Height:  |  Size: 9.3 MiB

+9360
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -19,15 +19,15 @@
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "npm:matej-appwrite-console-16x@0.6.7",
"@appwrite.io/console": "1.0.0",
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@popperjs/core": "^2.11.8",
"@stripe/stripe-js": "^3.5.0",
"ai": "^2.2.37",
"analytics": "^0.8.13",
"analytics": "^0.8.14",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.11",
"dayjs": "^1.11.12",
"deep-equal": "^2.2.3",
"echarts": "^5.5.1",
"envfile": "^7.1.0",
@@ -41,37 +41,37 @@
"devDependencies": {
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.45.2",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.5.18",
"@playwright/test": "^1.46.0",
"@sveltejs/adapter-static": "^3.0.4",
"@sveltejs/kit": "^2.5.22",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@testing-library/dom": "^10.3.2",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/svelte": "^5.2.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/svelte": "^5.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/deep-equal": "^1.0.4",
"@types/prismjs": "^1.26.4",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/ui": "^1.6.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.42.0",
"eslint-plugin-svelte": "^2.43.0",
"jsdom": "^22.1.0",
"kleur": "^4.1.5",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"sass": "^1.77.8",
"svelte": "^4.2.18",
"svelte-check": "^3.8.4",
"svelte-check": "^3.8.5",
"svelte-jester": "^2.3.2",
"svelte-preprocess": "^6.0.2",
"svelte-sequential-preprocessor": "^2.0.1",
"tslib": "^2.6.3",
"typescript": "^5.5.3",
"vite": "^5.3.4",
"typescript": "^5.5.4",
"vite": "^5.4.0",
"vitest": "^1.6.0"
},
"type": "module",
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
}
+616 -637
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -194,6 +194,7 @@ export enum Submit {
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
AuthSessionAlertsUpdate = 'submit_auth_session_alerts_update',
AuthMockNumbersUpdate = 'submit_auth_mock_numbers_update',
SessionsLengthUpdate = 'submit_sessions_length_update',
SessionsLimitUpdate = 'submit_sessions_limit_update',
@@ -318,5 +319,6 @@ export enum Submit {
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete',
ApplyQuickFilter = 'submit_apply_quick_filter'
}
@@ -16,7 +16,7 @@
}
</script>
{#if $paymentMissingMandate && $paymentMissingMandate.country === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes($page.url.pathname)}
{#if $paymentMissingMandate && $paymentMissingMandate?.country?.toLowerCase() === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes($page.url.pathname)}
<HeaderAlert title="Authorization required" type="info">
The payment method for {$organization.name} needs to be verified.
<svelte:fragment slot="buttons">
@@ -11,6 +11,7 @@
export let couponData: Partial<Coupon>;
export let billingBudget: number;
export let fixedCoupon = false; // If true, the coupon cannot be removed
export let isDowngrade = false;
const today = new Date();
const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
@@ -41,8 +42,8 @@
<p class="text">{formatCurrency(currentPlan.price)}</p>
</span>
<span class="u-flex u-main-space-between">
<p class="text">Additional seats ({collaborators?.length})</p>
<p class="text">
<p class="text" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
<p class="text" class:u-bold={isDowngrade}>
{formatCurrency(extraSeatsCost)}
</p>
</span>
+1
View File
@@ -6,3 +6,4 @@ export { default as EstimatedTotalBox } from './estimatedTotalBox.svelte';
export { default as PlanComparisonBox } from './planComparisonBox.svelte';
export { default as EmptyCardCloud } from './emptyCardCloud.svelte';
export { default as CreditsApplied } from './creditsApplied.svelte';
export { default as PlanSelection } from './planSelection.svelte';
@@ -1,30 +1,38 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { formatNum } from '$lib/helpers/string';
import { plansInfo } from '$lib/stores/billing';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
let selectedTab: 'tier-0' | 'tier-1' = 'tier-0';
let selectedTab: Tier = BillingPlan.FREE;
export let downgrade = false;
$: plan = $plansInfo.get(selectedTab);
</script>
<Card>
<SecondaryTabs stretch>
<SecondaryTabsItem
disabled={selectedTab === 'tier-0'}
on:click={() => (selectedTab = 'tier-0')}>
Free
</SecondaryTabsItem>
<SecondaryTabsItem
disabled={selectedTab === 'tier-1'}
on:click={() => (selectedTab = 'tier-1')}>
Pro
</SecondaryTabsItem>
</SecondaryTabs>
<Card style="--card-padding: 1.5rem">
<div class="comparison-box">
<SecondaryTabs stretch>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.FREE}
on:click={() => (selectedTab = BillingPlan.FREE)}>
{tierFree.name}
</SecondaryTabsItem>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.PRO}
on:click={() => (selectedTab = BillingPlan.PRO)}>
{tierPro.name}
</SecondaryTabsItem>
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.SCALE}
on:click={() => (selectedTab = BillingPlan.SCALE)}>
{tierScale.name}
</SecondaryTabsItem>
</SecondaryTabs>
</div>
<div class="u-margin-block-start-24">
{#if selectedTab === 'tier-0'}
{#if selectedTab === BillingPlan.FREE}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
{#if downgrade}
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
@@ -86,16 +94,50 @@
</li>
</ul>
{/if}
{:else if selectedTab === 'tier-1'}
{:else if selectedTab === BillingPlan.PRO}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
<ul class="un-order-list u-margin-block-start-8">
<li>Everything in the Free plan, plus:</li>
<p class="u-margin-block-start-8">Everything in the Free plan, plus:</p>
<ul class="un-order-list u-margin-inline-start-4">
<li>Unlimited databases, buckets, functions</li>
<li>{plan.bandwidth}GB bandwidth</li>
<li>{plan.storage}GB storage</li>
<li>{formatNum(plan.executions)} executions</li>
<li>Email support</li>
</ul>
{:else if selectedTab === BillingPlan.SCALE}
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
<ul class="un-order-list u-margin-inline-start-4">
<li>Unlimited seats</li>
<li>Organization roles <span class="inline-tag">Coming soon</span></li>
<li>SOC-2, HIPAA compliance</li>
<li>SSO <span class="inline-tag">Coming soon</span></li>
<li>Priority support</li>
</ul>
{/if}
</div>
</Card>
<style lang="scss">
.comparison-box {
border-radius: var(--border-radius-small);
background: hsl(var(--color-neutral-5));
}
:global(.theme-dark) .comparison-box {
background: hsl(var(--color-neutral-85));
}
.comparison-box :global(.secondary-tabs-button:where(:disabled)) {
background: hsl(var(--color-neutral-0));
border: 1px solid hsl(var(--color-neutral-10));
}
:global(.theme-dark) .comparison-box :global(.secondary-tabs-button:where(:disabled)) {
background: hsl(var(--color-neutral-80));
border: 1px solid hsl(var(--color-neutral-85));
}
.inline-tag {
line-height: 140%;
font-weight: 500;
}
</style>
+4 -2
View File
@@ -23,6 +23,7 @@
import { tooltip } from '$lib/actions/tooltip';
export let tier: Tier;
export let members: number;
const plan = $plansInfo?.get(tier);
let excess: {
@@ -41,7 +42,7 @@
$organization.billingCurrentInvoiceDate,
new Date().toISOString()
);
excess = calculateExcess(usage, plan, $organization);
excess = calculateExcess(usage, plan, members);
showExcess = Object.values(excess).some((value) => value > 0);
});
</script>
@@ -98,7 +99,8 @@
<TableCell title="excess">
<p class="u-color-text-danger">
<span class="icon-arrow-up" />
{humanFileSize(excess?.storage)}
{humanFileSize(excess?.storage).value}
{humanFileSize(excess?.storage).unit}
</p>
</TableCell>
</TableRow>
@@ -0,0 +1,98 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { formatCurrency } from '$lib/helpers/numbers';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { LabelCard } from '..';
export let billingPlan: Tier;
export let anyOrgFree = false;
export let isNewOrg = false;
let classes: string = '';
export { classes as class };
$: freePlan = $plansInfo.get(BillingPlan.FREE);
$: proPlan = $plansInfo.get(BillingPlan.PRO);
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
</script>
{#if billingPlan}
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={anyOrgFree}
value={BillingPlan.FREE}
tooltipShow={anyOrgFree}
tooltipText="You are limited to 1 Free organization per account."
padding={1.5}>
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={disabled}>
<h4 class="body-text-2 u-bold">
{tierFree.name}
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierFree.description}
</p>
<p>
{formatCurrency(freePlan?.price ?? 0)}
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
<li>
<LabelCard name="plan" bind:group={billingPlan} value={BillingPlan.PRO} padding={1.5}>
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{tierPro.name}
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierPro.description}
</p>
<p>
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
{#if $organization?.billingPlan === BillingPlan.SCALE}
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
value={BillingPlan.SCALE}
padding={1.5}>
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{tierScale.name}
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierScale.description}
</p>
<p>
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
{/if}
</ul>
{/if}
@@ -44,7 +44,7 @@
</script>
{#if filteredMethods?.length}
{#if selectedPaymentMethod?.country === 'in'}
{#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
+1 -1
View File
@@ -120,7 +120,7 @@
bind:this={tooltip}
class:u-width-full-line={fullWidth}
style:--arrow-size={`${arrowSize}px`}
style:z-index="10">
style:z-index="21">
<div
class="drop-arrow"
class:is-popover={isPopover}
+5 -4
View File
@@ -14,6 +14,8 @@
export let wrapperFullWidth = false;
export let position: 'relative' | 'static' = 'relative';
export let noMaxWidthList = false;
let classes: string = '';
export { classes as class };
</script>
<Drop
@@ -29,11 +31,10 @@
<slot />
<svelte:fragment slot="list">
<div
class="drop is-no-arrow"
class="drop is-no-arrow {classes}"
class:u-max-width-100-percent={fullWidth}
style={`${width ? `--drop-width-size-desktop:${width}rem; ` : ''} ${
position === 'static' ? 'position:static' : 'position:relative'
}`}>
style:--drop-width-size-desktop={width ? `${width}rem` : ''}
style:position>
{#if $$slots.list}
<section
class:u-overflow-y-auto={scrollable}
+3
View File
@@ -5,6 +5,7 @@
export let icon: string = null;
export let event: string = null;
export let loading = false;
export let padding: number | null = null;
function track() {
if (!event) {
@@ -20,6 +21,8 @@
<li class="drop-list-item">
<button
class="drop-button u-flex u-cross-center u-main-space-between"
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
on:click={track}
on:click|preventDefault
{disabled}>
+3 -15
View File
@@ -1,7 +1,5 @@
<script>
import { trackEvent } from '$lib/actions/analytics';
import { Button } from '$lib/elements/forms';
import { getNextTier, tierToPlan, upgradeURL } from '$lib/stores/billing';
import { getNextTier, tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import Card from './card.svelte';
@@ -21,22 +19,12 @@
</div>
</div>
{/if}
<div class="u-stretch u-flex-vertical">
<div class="u-stretch u-flex-vertical u-main-center">
<h3 class="body-text-2 u-bold"><slot name="title" /></h3>
<p class="u-margin-block-start-8">
<slot nextTier={tierToPlan(getNextTier($organization.billingPlan)).name} />
</p>
<Button
class="u-margin-block-start-32"
secondary
fullWidth
href={$upgradeURL}
on:click={() => {
trackEvent('click_organization_upgrade', {
from: 'button',
source
});
}}>Upgrade plan</Button>
<slot name="cta" {source} />
</div>
</div>
</Card>
+1 -1
View File
@@ -7,7 +7,7 @@
export let trimmed = true;
export let id: string = null;
export let style: string = undefined;
let classes: string = undefined;
let classes: string = '';
export { classes as class };
</script>
+6 -1
View File
@@ -137,7 +137,12 @@
bind:value />
{:else if column.type === 'datetime'}
{#key value}
<InputDateTime id="value" bind:value label="value" showLabel={false} />
<InputDateTime
id="value"
bind:value
label="value"
showLabel={false}
step={60} />
{/key}
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
+94 -44
View File
@@ -13,13 +13,19 @@
tags,
ValidOperators
} from './store';
import { createEventDispatcher } from 'svelte';
export let query = '[]';
export let columns: Writable<Column[]>;
export let disabled = false;
export let fullWidthMobile = false;
export let singleCondition = false;
export let clearOnClick = false; // When enabled the user doesn't have to click apply to clear the filters
export let enableApply = false;
export let quickFilters = false;
let displayQuickFilters = quickFilters;
const dispatch = createEventDispatcher();
const parsedQueries = queryParamToMap(query);
queries.set(parsedQueries);
@@ -44,10 +50,15 @@
function clearAll() {
selectedColumn = null;
queries.clearAll();
if (clearOnClick) {
queries.apply();
}
}
function apply() {
if (
if (quickFilters && displayQuickFilters) {
dispatch('apply');
} else if (
selectedColumn &&
operatorKey &&
(operatorKey === ValidOperators.IsNotNull ||
@@ -83,19 +94,22 @@
arrayValues = [];
}
$: isButtonDisabled = $queriesAreDirty
? false
: !selectedColumn ||
!operatorKey ||
(!value &&
!arrayValues.length &&
operatorKey !== ValidOperators.IsNotNull &&
operatorKey !== ValidOperators.IsNull);
$: isButtonDisabled =
$queriesAreDirty || (quickFilters && displayQuickFilters && enableApply)
? false
: !selectedColumn ||
!operatorKey ||
(!value &&
!arrayValues.length &&
operatorKey !== ValidOperators.IsNotNull &&
operatorKey !== ValidOperators.IsNull);
function toggleDropdown() {
dispatch('toggle', { show: !showFiltersDesktop });
showFiltersDesktop = !showFiltersDesktop;
}
function toggleMobileModal() {
dispatch('toggle', { show: !showFiltersMobile });
showFiltersMobile = !showFiltersMobile;
}
</script>
@@ -115,24 +129,43 @@
</slot>
<svelte:fragment slot="list">
<div class="dropped card">
<p>Apply filter rules to refine the table view</p>
<Content
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:arrayValues
{columns}
{singleCondition}
on:apply={afterApply}
on:clear={() => (applied = 0)} />
{#if displayQuickFilters}
<slot name="quick" />
{:else}
<p>Apply filter rules to refine the table view</p>
<Content
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:arrayValues
{columns}
{singleCondition}
on:apply={afterApply}
on:clear={() => (applied = 0)} />
{/if}
<hr />
<div class="u-flex u-margin-block-start-16 u-main-end u-gap-8">
{#if singleCondition}
<Button text on:click={toggleDropdown}>Cancel</Button>
{:else}
<Button text on:click={clearAll}>Clear all</Button>
<div
class="u-flex u-cross-center u-margin-block-start-16"
class:u-main-end={!quickFilters}
class:u-main-space-between={quickFilters}>
{#if quickFilters}
<Button
text
on:click={() => (displayQuickFilters = !displayQuickFilters)}
class="u-margin-block-end-auto">
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
</Button>
{/if}
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
<div class="u-flex u-gap-8">
{#if singleCondition}
<Button text on:click={toggleDropdown}>Cancel</Button>
{:else}
<Button disabled={applied === 0} text on:click={clearAll}>
Clear all
</Button>
{/if}
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
</div>
</div>
</div>
</svelte:fragment>
@@ -141,10 +174,7 @@
<div class="is-only-mobile">
<slot name="mobile" {disabled} toggle={toggleMobileModal}>
<Button
secondary
on:click={() => (showFiltersMobile = !showFiltersMobile)}
{fullWidthMobile}>
<Button secondary on:click={toggleMobileModal} {fullWidthMobile}>
<i class="icon-filter u-opacity-50" />
Filters
{#if applied > 0}
@@ -160,22 +190,42 @@
description="Apply filter rules to refine the table view"
bind:show={showFiltersMobile}
size="big">
<Content
{columns}
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:arrayValues
{singleCondition}
on:apply={afterApply}
on:clear={() => (applied = 0)} />
{#if displayQuickFilters}
<slot name="quick" />
{:else}
<Content
{columns}
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:arrayValues
{singleCondition}
on:apply={afterApply}
on:clear={() => (applied = 0)} />
{/if}
<svelte:fragment slot="footer">
{#if singleCondition}
<Button text on:click={() => (showFiltersMobile = false)}>Cancel</Button>
{:else}
<Button text on:click={clearAll}>Clear all</Button>
{/if}
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
<div
class="u-flex u-cross-center u-width-full-line"
class:u-main-end={!quickFilters}
class:u-main-space-between={quickFilters}>
{#if quickFilters}
<Button
text
noMargin
on:click={() => (displayQuickFilters = !displayQuickFilters)}
class="u-margin-block-end-auto">
{displayQuickFilters ? 'Advanced filters' : 'Quick filters'}
</Button>
{/if}
<div class="u-flex u-gap-8">
{#if singleCondition}
<Button text on:click={() => (showFiltersMobile = false)}>Cancel</Button>
{:else}
<Button text on:click={clearAll}>Clear all</Button>
{/if}
<Button on:click={apply} disabled={isButtonDisabled}>Apply</Button>
</div>
</div>
</svelte:fragment>
</Modal>
</div>
+18 -12
View File
@@ -13,27 +13,23 @@ export type TagValue = {
};
export type Operator = {
toTag: (
attribute: string,
input?: string | number | string[],
type?: string
) => string | TagValue;
toTag: (attribute: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (attribute: string, input?: string | number | string[]) => string;
types: ColumnType[];
hideInput?: boolean;
};
export function mapToQueryParams(map: Map<string | TagValue, string>) {
export function mapToQueryParams(map: Map<TagValue, string>) {
return encodeURIComponent(JSON.stringify(Array.from(map.entries())));
}
export function queryParamToMap(queryParam: string) {
const decodedQueryParam = decodeURIComponent(queryParam);
const queries = JSON.parse(decodedQueryParam) as [string, string][];
const queries = JSON.parse(decodedQueryParam) as [TagValue, string][];
return new Map(queries);
}
function initQueries(initialValue = new Map<string | TagValue, string>()) {
function initQueries(initialValue = new Map<TagValue, string>()) {
const queries = writable(initialValue);
type AddFilterArgs = {
@@ -52,7 +48,7 @@ function initQueries(initialValue = new Map<string | TagValue, string>()) {
});
}
function removeFilter(tag: string | TagValue) {
function removeFilter(tag: TagValue) {
queries.update((map) => {
map.delete(tag);
return map;
@@ -256,16 +252,22 @@ function generateDefaultOperators() {
toQuery: operator.query,
toTag: (attribute, input = null, type = null) => {
if (input === null) {
return `**${attribute}** ${operatorName}`;
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 `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`;
return {
value: input,
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
};
} else {
return `**${attribute}** ${operatorName} **${input}**`;
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
}
},
types: operator.types,
@@ -276,3 +278,7 @@ function generateDefaultOperators() {
}
export const operators = generateDefaultOperators();
export function tagFormat(node: HTMLElement) {
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
}
+17 -47
View File
@@ -1,53 +1,23 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { queries, tags, type TagValue } from './store';
function tagFormat(node: HTMLElement) {
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
}
// We cast to any to not cause type errors in the input components
/* eslint @typescript-eslint/no-explicit-any: 'off' */
function isTypeTagValue(obj: any): obj is TagValue {
if (typeof obj === 'string') return false;
return (
obj &&
typeof obj.tag === 'string' &&
(typeof obj.value === 'string' ||
typeof obj.value === 'number' ||
Array.isArray(obj.value))
);
}
import { queries, tagFormat, tags } from './store';
</script>
{#each $tags as tag (tag)}
{#if isTypeTagValue(tag)}
<button
use:tooltip={{
content: tag?.value?.toString()
}}
class="tag"
on:click={() => {
queries.removeFilter(tag);
queries.apply();
}}>
<span class="text" use:tagFormat>
{tag.tag}
</span>
<i class="icon-x" />
</button>
{:else}
<button
class="tag"
on:click={() => {
queries.removeFilter(tag);
queries.apply();
}}>
<span class="text" use:tagFormat>
{tag}
</span>
<i class="icon-x" />
</button>
{/if}
<button
use:tooltip={{
content: tag?.value?.toString(),
disabled: Array.isArray(tag.value) ? tag.value?.length < 3 : true
}}
type="button"
class="tag"
on:click|preventDefault={() => {
queries.removeFilter(tag);
queries.apply();
}}>
<span class="text" use:tagFormat>
{tag.tag}
</span>
<i class="icon-x" />
</button>
{/each}
+1 -1
View File
@@ -28,7 +28,7 @@
:global(.theme-dark) .floating-action-bar {
border: 1px solid hsl(var(--color-neutral-85));
background: hsl(var(--color-neutral-90));
box-shadow: 0px 6px 16px 8px #14141f;
box-shadow: 0px 6px 16px 8px hsl(var(--color-neutral-105));
}
:global(.theme-light) .floating-action-bar {
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
export let large = false;
export let stretch = false;
let classes: string = undefined;
let classes: string = '';
export { classes as class };
</script>
+1 -1
View File
@@ -17,7 +17,7 @@
$organization?.billingPlan === BillingPlan.PRO ||
$organization?.billingPlan === BillingPlan.SCALE;
$: supportTimings = `${utcHourToLocaleHour('09:00')} - ${utcHourToLocaleHour('17:00')} ${localeTimezoneName()}`;
$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeTimezoneName()}`;
</script>
{#if isCloud}
+77 -38
View File
@@ -65,200 +65,239 @@ export const scopes: {
scope: string;
description: string;
category: string;
icon: string;
}[] = [
{
scope: 'sessions.write',
description: "Access to create, update and delete your project's sessions",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'users.read',
description: "Access to read your project's users",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'users.write',
description: "Access to create, update, and delete your project's users",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'teams.read',
description: "Access to read your project's teams",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'teams.write',
description: "Access to create, update, and delete your project's teams",
category: 'Auth'
category: 'Auth',
icon: 'user-group'
},
{
scope: 'databases.read',
description: "Access to read your project's databases",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'databases.write',
description: "Access to create, update, and delete your project's databases",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'collections.read',
description: "Access to read your project's database collections",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'collections.write',
description: "Access to create, update, and delete your project's database collections",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.read',
description: "Access to read your project's database collection's attributes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'attributes.write',
description:
"Access to create, update, and delete your project's database collection's attributes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.read',
description: "Access to read your project's database collection's indexes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'indexes.write',
description:
"Access to create, update, and delete your project's database collection's indexes",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'documents.read',
description: "Access to read your project's database documents",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'documents.write',
description: "Access to create, update, and delete your project's database documents",
category: 'Database'
category: 'Database',
icon: 'database'
},
{
scope: 'files.read',
description: "Access to read your project's storage files and preview images",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'files.write',
description: "Access to create, update, and delete your project's storage files",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'buckets.read',
description: "Access to read your project's storage buckets",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'buckets.write',
description: "Access to create, update, and delete your project's storage buckets",
category: 'Storage'
category: 'Storage',
icon: 'folder'
},
{
scope: 'functions.read',
description: "Access to read your project's functions and code deployments",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'functions.write',
description:
"Access to create, update, and delete your project's functions and code deployments",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'execution.read',
description: "Access to read your project's execution logs",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'execution.write',
description: "Access to execute your project's functions",
category: 'Functions'
category: 'Functions',
icon: 'lightning-bolt'
},
{
scope: 'targets.read',
description: "Access to read your project's messaging targets",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'targets.write',
description: "Access to create, update, and delete your project's messaging targets",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'providers.read',
description: "Access to read your project's messaging providers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'providers.write',
description: "Access to create, update, and delete your project's messaging providers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'messages.read',
description: "Access to read your project's messages",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'messages.write',
description: "Access to create, update, and delete your project's messages",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'topics.read',
description: "Access to read your project's messaging topics",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'topics.write',
description: "Access to create, update, and delete your project's messaging topics",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'subscribers.read',
description: "Access to read your project's messaging topic subscribers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'subscribers.write',
description:
"Access to create, update, and delete your project's messaging topic subscribers",
category: 'Messaging'
category: 'Messaging',
icon: 'send'
},
{
scope: 'locale.read',
description: "Access to access your project's Locale service",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'avatars.read',
description: "Access to access your project's Avatars service",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'health.read',
description: "Access to read your project's health status",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'migrations.read',
description: "Access to read your project's migration status",
category: 'Other'
category: 'Other',
icon: 'globe'
},
{
scope: 'migrations.write',
description: 'Access to create migrations',
category: 'Other'
category: 'Other',
icon: 'globe'
}
];
+1 -1
View File
@@ -7,7 +7,7 @@
export let width = 40;
export let height = 30;
export let quality = 100;
let classes: string = undefined;
let classes: string = '';
export { classes as class };
export function getFlag(country: string, width: number, height: number, quality: number) {
+1 -1
View File
@@ -21,7 +21,7 @@
export let ariaLabel: string = null;
export let noMargin = false;
export let event: string = null;
let classes: string = undefined;
let classes: string = '';
export { classes as class };
export let actions: MultiActionArray = [];
export let submissionLoader = false;
+12 -1
View File
@@ -18,7 +18,18 @@
class:icon-exclamation={type === 'warning'}
aria-hidden="true" />
{/if}
<span class="text">
<span class="text u-line-height-1-5">
<slot />
</span>
</p>
<style lang="scss">
.icon-info,
.icon-exclamation-circle,
.icon-check-circle,
.icon-exclamation {
&::before {
vertical-align: baseline;
}
}
</style>
+1
View File
@@ -20,6 +20,7 @@ export { default as InputSelectSearch } from './inputSelectSearch.svelte';
export { default as InputCheckbox } from './inputCheckbox.svelte';
export { default as InputChoice } from './inputChoice.svelte';
export { default as InputPhone } from './inputPhone.svelte';
export { default as InputOTP } from './inputOTP.svelte';
export { default as InputCron } from './inputCron.svelte';
export { default as InputURL } from './inputURL.svelte';
export { default as InputId } from './inputId.svelte';
+2 -1
View File
@@ -14,6 +14,7 @@
export let readonly = false;
export let autofocus = false;
export let autocomplete = false;
export let step: number | 'any' = 0.001;
let element: HTMLInputElement;
let error: string;
@@ -70,7 +71,7 @@
{readonly}
{required}
{value}
step=".001"
{step}
autocomplete={autocomplete ? 'on' : 'off'}
type="datetime-local"
class="input-text"
+6 -5
View File
@@ -32,7 +32,7 @@
}
function isFileExtensionAllowed(fileExtension: string) {
if (allowedFileExtensions.length && !allowedFileExtensions.includes(fileExtension)) {
if (allowedFileExtensions?.length && !allowedFileExtensions.includes(fileExtension)) {
return false;
}
return true;
@@ -42,7 +42,7 @@
hovering = false;
if (!ev.dataTransfer.items) return;
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
const fileExtension = ev.dataTransfer.items[i].getAsFile().name.split('.')[1];
const fileExtension = ev.dataTransfer.items[i].getAsFile().name.split('.').pop();
if (!isFileExtensionAllowed(fileExtension)) {
error = 'Invalid file extension';
return;
@@ -183,15 +183,16 @@
{#if files?.length}
<ul class="upload-file-box-list u-min-width-0">
{#each fileArray as file}
{@const fileName = file.name.split('.')}
{@const fileName = file.name.split('.').slice(0, -1).join('.')}
{@const extension = file.name.split('.').pop()}
{@const fileSize = humanFileSize(file.size)}
<li class="u-flex u-cross-center u-min-width-0">
<span class="icon-document" aria-hidden="true" />
<span class="upload-file-box-name u-trim u-min-width-0">
<Trim>{fileName[0]}</Trim>
<Trim>{fileName}</Trim>
</span>
<span class="upload-file-box-name u-min-width-0 u-flex-shrink-0">
.{fileName[1]}
.{extension}
</span>
<span
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
+121
View File
@@ -0,0 +1,121 @@
<script lang="ts">
import { SvelteComponent, onMount } from 'svelte';
import { FormItem, FormItemPart, Helper, Label } from '.';
import { Drop } from '$lib/components';
export let label: string = undefined;
export let optionalText: string | undefined = undefined;
export let showLabel = true;
export let id: string;
export let name: string = id;
export let value = '';
export let pattern: string = null;
export let patternError: string = '';
export let placeholder = '';
export let required = false;
export let hideRequired = false;
export let disabled = false;
export let readonly = false;
export let maxlength: number = null;
export let autofocus = false;
export let autocomplete = false;
export let fullWidth = false;
export let tooltip: string = null;
export let isMultiple = false;
export let popover: typeof SvelteComponent<unknown> = null;
export let popoverProps: Record<string, unknown> = {};
let element: HTMLInputElement;
let error: string;
let show = false;
onMount(() => {
if (element && autofocus) {
element.focus();
}
});
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
if (patternError && element.validity.patternMismatch) {
error = patternError;
return;
}
error = element.validationMessage;
};
$: if (value) {
error = null;
}
type $$Events = {
input: Event & { target: HTMLInputElement };
};
$: wrapper = isMultiple ? FormItemPart : FormItem;
</script>
<svelte:component this={wrapper} {fullWidth}>
{#if label}
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
{label}{#if popover}
<Drop isPopover bind:show display="inline-block">
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style:font-size="var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--p-card-padding=".75rem"
style:--card-border-radius="var(--border-radius-small)"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
<div class="input-text-wrapper">
<input
{id}
{name}
{placeholder}
{disabled}
{readonly}
{required}
{maxlength}
{pattern}
type="text"
autocomplete={autocomplete ? 'on' : 'off'}
class="input-text"
bind:value
bind:this={element}
on:invalid={handleInvalid} />
{#if $$slots.options}
<div class="options-list">
<slot name="options" />
</div>
{/if}
<slot />
</div>
{#if error}
<Helper type="warning" class="u-line-height-1">{error}</Helper>
{/if}
</svelte:component>
+17 -5
View File
@@ -19,7 +19,7 @@
export let popoverProps: Record<string, unknown> = {};
export let fullWidth = false;
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
const pattern = String.raw`^\+[1-9]\d{1,14}$`;
let element: HTMLInputElement;
let error: string;
@@ -34,15 +34,21 @@
const handleInvalid = (event: Event) => {
event.preventDefault();
if (element.validity.patternMismatch) {
error = "Allowed characters: leading '+' and maximum of 15 digits";
return;
}
if (element.validity.valueMissing) {
error = 'This field is required';
return;
}
if (element.validity.patternMismatch) {
error = `Allowed characters: leading '+' and maximum of ${maxlength - 1} digits`;
return;
}
if (element.validity.tooShort) {
error = `The value must contain leading + and at least ${minlength - 1} digits `;
return;
}
error = element.validationMessage;
};
@@ -92,7 +98,13 @@
autocomplete={autocomplete ? 'on' : 'off'}
bind:value
bind:this={element}
style:--amount-of-buttons={$$slots.options ? 1 : 0}
on:invalid={handleInvalid} />
{#if $$slots.options}
<div class="options-list">
<slot name="options" />
</div>
{/if}
</div>
{#if error}
<Helper type="warning">{error}</Helper>
@@ -26,6 +26,8 @@
export let fullWidth = false;
export let autofocus = false;
export let interactiveOutput = false;
export let interactiveEmpty = false;
export let hideEmpty = false;
// stretch is used inside of a flex container to give the element flex:1
export let stretch = true;
export let search = '';
@@ -51,7 +53,8 @@
});
function handleInput() {
hasFocus = true;
if (hideEmpty && !options?.length) hasFocus = false;
else hasFocus = true;
}
function handleKeydown(event: KeyboardEvent) {
@@ -202,7 +205,18 @@
</li>
{:else}
<li class="drop-list-item">
<span class="text">There are no {name} that match your search</span>
<button
class:drop-button={interactiveEmpty}
type="button"
on:click={() => {
if (interactiveOutput && interactiveEmpty) hasFocus = !hasFocus;
}}>
<slot name="empty">
<span class="text">
There are no {name} that match your search
</span>
</slot>
</button>
</li>
{/each}
</svelte:fragment>
+1 -1
View File
@@ -36,7 +36,7 @@
on:click|preventDefault
class="tooltip"
aria-label="input tooltip"
use:tooltipAction={{ content: tooltip }}>
use:tooltipAction={{ content: tooltip, appendTo: 'parent' }}>
<span class="icon-info" aria-hidden="true" style="font-size: var(--icon-size-small)" />
</button>
{/if}
+1
View File
@@ -2,3 +2,4 @@ export { default as Pill } from './pill.svelte';
export { default as SelectSearchItem } from './selectSearchItem.svelte';
export { default as Flag } from './flag.svelte';
export { default as SelectSearchCheckbox } from './selectSearchCheckbox.svelte';
export { default as SelectSearchRadio } from './selectSearchRadio.svelte';
+12 -4
View File
@@ -12,6 +12,10 @@
export let external = false;
export let href: string = null;
export let event: string = null;
export let eventData: Record<string, unknown> = {};
export let style = '';
let classes = '';
export { classes as class };
function track() {
if (!event) {
@@ -19,17 +23,19 @@
}
trackEvent(`click_${event}`, {
from: 'tag'
from: 'tag',
...eventData
});
}
</script>
{#if href}
<a
{style}
{href}
target={external ? '_blank' : '_self'}
rel={external ? 'noopener noreferrer' : ''}
class="tag"
class="tag {classes}"
class:is-disabled={disabled}
class:is-selected={selected}
class:is-success={success}
@@ -42,9 +48,10 @@
<button
on:click
on:click={track}
{style}
{disabled}
type={submit ? 'submit' : 'button'}
class="tag"
class="tag {classes}"
class:is-disabled={disabled}
class:is-selected={selected}
class:is-success={success}
@@ -55,7 +62,8 @@
</button>
{:else}
<div
class="tag"
class="tag {classes}"
{style}
class:is-disabled={disabled}
class:is-selected={selected}
class:is-success={success}
+3 -1
View File
@@ -1,11 +1,13 @@
<script lang="ts">
// export let data: string[] = [];
export let value = false;
export let padding: number | null = null;
</script>
<li class="drop-list-item">
<button
class="drop-button u-flex u-cross-center u-gap-12"
style:--button-padding-horizontal={padding ? `${padding / 16}rem` : ''}
style:--button-padding-vertical={padding ? `${padding / 16}rem` : ''}
on:click|preventDefault
type="button">
<input type="checkbox" class="is-small" bind:checked={value} />
+14
View File
@@ -0,0 +1,14 @@
<script lang="ts">
export let value: string;
export let group: string;
</script>
<li class="drop-list-item">
<button
class="drop-button u-flex u-cross-center u-gap-12"
on:click|preventDefault
type="button">
<input type="radio" class="is-small" {value} bind:group />
<slot />
</button>
</li>
+26
View File
@@ -0,0 +1,26 @@
<script lang="ts">
export let title = '';
export let onlyDesktop = false;
export let width: number = null;
export let showOverflow = false;
let className = '';
export { className as class };
export let style = '';
export let right = false;
</script>
<button
{style}
style:--p-col-width={width?.toString() ?? ''}
class="table-col {className}"
class:u-overflow-visible={showOverflow}
class:is-only-desktop={onlyDesktop}
class:u-flex={right}
class:u-main-end={right}
data-title={title}
role="cell"
on:click
on:keypress
data-private>
<slot />
</button>
+4 -16
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { toggle } from '$lib/helpers/array';
import { isHTMLInputElement } from '$lib/helpers/types';
import { TableCell } from '.';
import { TableCellButton } from '.';
import { InputCheckbox } from '../forms';
export let id: string;
@@ -12,6 +12,7 @@
const handleClick = (e: Event) => {
// Prevent the link from being followed
e.preventDefault();
e.stopPropagation();
if (!isHTMLInputElement(el)) return;
selectedIds = toggle(selectedIds, id);
@@ -24,13 +25,7 @@
};
</script>
<TableCell class="u-position-relative">
<div
class="touch-area"
role="button"
tabindex="-1"
on:click={handleClick}
on:keypress={handleClick} />
<TableCellButton on:click={handleClick}>
<InputCheckbox
bind:element={el}
id="select-{id}"
@@ -38,11 +33,4 @@
checked={selectedIds.includes(id)}
{disabled}
on:click={handleClick} />
</TableCell>
<style lang="scss">
.touch-area {
position: absolute;
inset: 0;
}
</style>
</TableCellButton>
+1
View File
@@ -8,6 +8,7 @@ export { default as TableRow } from './row.svelte';
export { default as TableRowLink } from './rowLink.svelte';
export { default as TableRowButton } from './rowButton.svelte';
export { default as TableCell } from './cell.svelte';
export { default as TableCellButton } from './cellButton.svelte';
export { default as TableCellHead } from './cellHead.svelte';
export { default as TableCellHeadCheck } from './cellHeadCheck.svelte';
export { default as TableCellLink } from './cellLink.svelte';
+1 -1
View File
@@ -7,7 +7,7 @@
export let transparent = false;
export let noStyles = false;
export let dense = false;
let classes: string = undefined;
let classes: string = '';
export { classes as class };
let isOverflowing = false;
+1 -1
View File
@@ -38,7 +38,7 @@ export type Column = {
filter?: boolean;
array?: boolean;
format?: string;
elements?: string[] | { value: string; label: string }[];
elements?: string[] | { value: string | number; label: string }[];
};
export function isValueOfStringEnum<T extends Record<string, string>>(
+2 -2
View File
@@ -7,10 +7,10 @@
export let title: string;
export let tooltipContent =
$organization.billingPlan === BillingPlan.FREE
$organization?.billingPlan === BillingPlan.FREE
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization.billingPlan).name
tierToPlan($organization?.billingPlan)?.name
} plan`;
export let disabled: boolean;
export let buttonText: string;
+44 -12
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { hoursToDays, toLocaleDateTime } from '$lib/helpers/date';
import { hoursToDays, timeFromNow, toLocaleDateTime } from '$lib/helpers/date';
import { log } from '$lib/stores/logs';
import { Alert, Card, Code, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
import { Alert, Card, Code, Copy, Heading, Id, SvgIcon, Tab, Tabs } from '../components';
import { calculateTime } from '$lib/helpers/timeConversion';
import {
TableBody,
@@ -18,6 +18,7 @@
import { organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { BillingPlan } from '$lib/constants';
import { tooltip } from '$lib/actions/tooltip';
let selectedRequest = 'parameters';
let selectedResponse = 'logs';
@@ -124,13 +125,27 @@
{/if}
</ul>
<div class="status u-margin-inline-start-auto">
<Pill
warning={execution.status === 'waiting' ||
execution.status === 'building'}
danger={execution.status === 'failed'}
info={execution.status === 'completed' || execution.status === 'ready'}>
{execution.status}
</Pill>
<div
use:tooltip={{
content: `Scheduled to execute on ${toLocaleDateTime(execution.scheduledAt)}`,
disabled:
!execution?.scheduledAt || execution.status !== 'scheduled',
maxWidth: 180
}}>
<Pill
warning={execution.status === 'waiting' ||
execution.status === 'building'}
danger={execution.status === 'failed'}
info={execution.status === 'completed' ||
execution.status === 'ready'}>
{#if execution.status === 'scheduled'}
<span class="icon-clock" aria-hidden="true" />
{timeFromNow(execution.scheduledAt)}
{:else}
{execution.status}
{/if}
</Pill>
</div>
</div>
</div>
</div>
@@ -145,7 +160,23 @@
</div>
<div class="u-flex u-gap-16">
<h4 class="text u-bold">Path:</h4>
<span class="u-text-color-gray">{execution.requestPath}</span>
<Copy value={execution.requestPath}>
<div
class="interactive-text-output is-textarea"
style:min-inline-size="0">
<span class="text u-line-height-1-5 u-break-all">
{execution.requestPath}
</span>
<div class="u-flex u-cross-child-start u-gap-8">
<button
class="interactive-text-output-button"
aria-label="copy text">
<span class="icon-duplicate" aria-hidden="true" />
</button>
</div>
</div>
</Copy>
</div>
</div>
@@ -156,8 +187,9 @@
</div>
<div class="u-flex u-gap-16">
<h4 class="text u-bold">Status Code:</h4>
<span class="u-text-color-gray"
>{execution.responseStatusCode}</span>
<span class="u-text-color-gray">
{execution.responseStatusCode}
</span>
</div>
</div>
</header>
+5
View File
@@ -12,6 +12,8 @@
import { isCloud } from '$lib/system';
import Create from '$routes/(console)/feedbackWizard.svelte';
import { showSupportModal } from '$routes/(console)/wizard/support/store';
import { getContext } from 'svelte';
import type { Writable } from 'svelte/store';
export let isOpen = false;
@@ -29,6 +31,9 @@
narrow = hasSubNavigation;
}
$: getContext<Writable<boolean>>('isNarrow').set(narrow);
$: getContext<Writable<boolean>>('hasSubNavigation').set(hasSubNavigation);
function handleKeyDown(event: KeyboardEvent) {
// If Alt + S is pressed
if (hasSubNavigation && event.altKey && event.keyCode === 83) {
+8 -1
View File
@@ -5,6 +5,8 @@
import { log } from '$lib/stores/logs';
import { wizard } from '$lib/stores/wizard';
import { activeHeaderAlert } from '$routes/(console)/store';
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
export let isOpen = false;
export let showSideNavigation = false;
@@ -40,6 +42,10 @@
wizard.hide();
}
});
const isNarrow = setContext('isNarrow', writable(false));
const hasSubNavigation = setContext('hasSubNavigation', writable(false));
$: sideSize = $hasSubNavigation ? ($isNarrow ? '17rem' : '25rem') : '12.5rem';
</script>
<svelte:window bind:scrollY={y} />
@@ -48,7 +54,8 @@
class:grid-with-side={showSideNavigation}
class:is-open={isOpen}
class:u-hide={$wizard.show || $log.show || $wizard.cover}
class:is-fixed-layout={$activeHeaderAlert?.show}>
class:is-fixed-layout={$activeHeaderAlert?.show}
style:--p-side-size={sideSize}>
{#if $activeHeaderAlert?.show}
<svelte:component this={$activeHeaderAlert.component} />
{/if}
+13 -2
View File
@@ -139,7 +139,18 @@
}
$: sortedSteps = [...steps].sort(([a], [b]) => (a > b ? 1 : -1));
$: isLastStep = $wizard.step === steps.size;
$: isLastStep = (() => {
if ($wizard.step === steps.size) {
return true;
}
let lastStep = true;
steps.forEach((step, i) => {
if (i > $wizard.step && !step.disabled) {
lastStep = false;
}
});
return lastStep;
})();
$: currentStep = steps.get($wizard.step);
</script>
@@ -187,7 +198,7 @@
<svelte:component this={component} />
{/if}
{/each}
<div class="u-z-index-20 form-footer">
<div class="u-z-index-5 form-footer">
<div class="u-flex u-main-end u-gap-12">
{#if !isLastStep && currentStep?.optional}
<Button text on:click={() => dispatch('finish')}>
+9 -2
View File
@@ -31,17 +31,23 @@ export type Invoice = {
$id: string;
$createdAt: Date;
$updatedAt: Date;
permissions: string[];
teamId: string;
aggregationId: string;
plan: Tier;
amount: number;
tax: number;
taxAmount: number;
vat: number;
vatAmount: number;
grossAmount: number;
creditsUsed: number;
currency: string;
date: number;
from: string;
to: string;
status: string;
dueAt: string;
clientSecret: string;
tier: Tier;
usage: {
name: string;
value: number /* service over the limit*/;
@@ -111,6 +117,7 @@ export type Credit = {
};
export type CreditList = {
available: number;
credits: Credit[];
total: number;
};
+4 -4
View File
@@ -150,7 +150,7 @@ export const tierPro: TierData = {
};
export const tierScale: TierData = {
name: 'Scale',
description: 'For scaling teams that need dedicated support.'
description: 'For scaling teams and agencies that need dedicated support.'
};
export const showUsageRatesModal = writable<boolean>(false);
@@ -362,7 +362,7 @@ export async function checkForMandate(org: Organization) {
const paymentId = org.paymentMethodId ?? org.backupPaymentMethodId;
if (!paymentId) return;
const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentId);
if (paymentMethod.mandateId === null && paymentMethod.country === 'in') {
if (paymentMethod?.mandateId === null && paymentMethod?.country.toLowerCase() === 'in') {
headerAlert.add({
id: 'paymentMandate',
component: PaymentMandate,
@@ -422,14 +422,14 @@ export const upgradeURL = derived(
export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account'];
export function calculateExcess(usage: OrganizationUsage, plan: Plan, org: Organization) {
export function calculateExcess(usage: OrganizationUsage, plan: Plan, members: number) {
const totBandwidth = usage?.bandwidth?.length > 0 ? last(usage.bandwidth).value : 0;
return {
bandwidth: calculateResourceSurplus(totBandwidth, plan.bandwidth),
storage: calculateResourceSurplus(usage?.storageTotal, plan.storage, 'GB'),
users: calculateResourceSurplus(usage?.usersTotal, plan.users),
executions: calculateResourceSurplus(usage?.executionsTotal, plan.executions, 'GB'),
members: calculateResourceSurplus(org.total, plan.members)
members: calculateResourceSurplus(members, plan.members)
};
}
+64 -4
View File
@@ -1,11 +1,14 @@
//campaign welcome and startup
import { BillingPlan } from '$lib/constants';
export type CampaignData = {
title: string;
description: string;
template: 'card' | 'review';
data?: Record<string, unknown>;
onlyNewOrgs?: boolean;
plan?: BillingPlan;
footer?: boolean;
};
@@ -22,7 +25,7 @@ campaigns
template: 'card',
title: 'Welcome to the Startups program!',
description:
"We're excited to have you on board. Add the coupon code to your Appwrite Pro account to join."
"We're excited to have you on board. Add the coupon code to your Appwrite Scale account to join."
})
.set('RenderATL2024', {
template: 'card',
@@ -52,12 +55,33 @@ campaigns
template: 'review',
title: 'Welcome to the Startups program',
description:
"We're excited to have you on board. Add your credit code to your Appwrite Pro account to join.",
"We're excited to have you on board. Add your credit code to your Appwrite Scale account to join.",
onlyNewOrgs: true,
plan: BillingPlan.SCALE,
data: {
cta: 'Get everything out of Cloud with Scale',
claimed: 'Your credits will be valid for 12 months.',
unclaimed: 'Apply your code to join the Startups program.',
reviews: [
{
name: 'David Foster',
img: '1.jpeg',
desc: 'Managing director',
review: 'We really loved working with Appwrite for launching our bootstrapped "Open Mind" App. It was saving us a lot of money in comparison to Firebase since the amount of users grew quite fast and we needed a quick switch.'
}
]
}
})
.set('Free100', {
template: 'review',
title: 'Get started with $100 credits',
description:
'Get $100 in Cloud credits when you upgrade or create an organization with a Pro plan.',
onlyNewOrgs: true,
data: {
cta: 'Get everything out of Cloud with Pro',
claimed: 'Your credits will be valid for 12 months.',
unclaimed: 'Apply your code to join the Startups program.',
claimed: 'Your credits will be valid for 3 months.',
unclaimed: 'Apply your code to join Appwrite Pro.',
reviews: [
{
name: 'David Foster',
@@ -84,6 +108,42 @@ campaigns
]
}
})
.set('FreeCodeCamp', {
template: 'card',
title: 'Claim your $50 FreeCodeCamp credits.',
description:
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
})
.set('AniaKubow', {
template: 'card',
title: 'Claim your $50 Ania Kubów credits.',
description:
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
})
.set('Fireship', {
template: 'card',
title: 'Claim your $50 Fireship credits.',
description:
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
})
.set('Hyperplexed', {
template: 'card',
title: 'Claim your $50 Hyperplexed credits.',
description:
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
})
.set('TraversyMedia', {
template: 'card',
title: 'Claim your $50 TraversyMedia credits.',
description:
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
})
.set('VueJS', {
template: 'card',
title: 'Claim your $50 VueJS credits.',
description:
'Get $50 in Cloud credits when you upgrade or create an organization with a Pro plan'
})
.set('FusionVC', {
template: 'review',
title: 'Welcome to Appwrite!',
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,7 +19,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
billingAddressId?: string;
amount: number;
billingTaxId?: string;
billingPlanDowngrade?: string;
billingPlanDowngrade?: Tier;
};
export type OrganizationList = {
+17
View File
@@ -0,0 +1,17 @@
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import { derived } from 'svelte/store';
export const runtimesList = derived(
page,
async ($page) => (await $page.data.runtimesList) as Models.RuntimeList
);
export const baseRuntimesList = derived(runtimesList, async ($runtimesList) => {
const baseRuntimes = new Map<string, Models.Runtime>();
for (const runtime of (await $runtimesList).runtimes) {
const runtimeBase = runtime.name.split('-')[0];
baseRuntimes.set(runtimeBase, runtime);
}
return { runtimes: [...baseRuntimes.values()] };
});
+20
View File
@@ -0,0 +1,20 @@
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import { derived } from 'svelte/store';
export const templatesList = derived(
page,
async ($page) => (await $page.data.templatesList) as Models.TemplateFunctionList
);
export const featuredTemplatesList = derived(templatesList, async ($templatesList) => {
return {
templates: (await $templatesList).templates
.filter((template) => template.id !== 'starter')
.slice(0, 2)
};
});
export const starterTemplate = derived(templatesList, async ($templatesList) => {
return (await $templatesList).templates.find((template) => template.id === 'starter');
});
@@ -1,39 +0,0 @@
<script lang="ts">
import { Box } from '$lib/components';
import { FormList, Helper, InputChoice, InputPassword } from '$lib/elements/forms';
import type { Variable } from '$lib/stores/marketplace';
import { templateConfig } from '../store';
export let appwriteVariable: Variable;
</script>
<Box radius="small" padding={16}>
<FormList>
<div>
<InputPassword
id={appwriteVariable.name}
label={appwriteVariable.name}
placeholder={appwriteVariable.placeholder ?? 'Enter value'}
showPasswordButton
required={appwriteVariable.required && !$templateConfig.generateKey}
bind:value={$templateConfig.appwriteApiKey}
disabled={!!$templateConfig.generateKey} />
<Helper type="neutral">
This API key will allow you to interact with the Appwrite server APIs. <a
href="https://appwrite.io/docs/advanced/platform/api-keys"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</Helper>
</div>
<InputChoice
bind:value={$templateConfig.generateKey}
id="generate"
label="Generate API key on completion"
disabled={!!$templateConfig.appwriteApiKey}>
The <code class="inline-code">APPWRITE_API_KEY</code> will be automatically generated for
your project and applied to this function once it's created.
</InputChoice>
</FormList>
</Box>
+98 -86
View File
@@ -1,7 +1,10 @@
<script context="module" lang="ts">
import CreateTemplate from './createTemplate.svelte';
export function connectTemplate(template: MarketplaceTemplate, runtime: string | null = null) {
export function connectTemplate(
template: Models.TemplateFunction,
runtime: string | null = null
) {
const variables: Record<string, string> = {};
template.variables.forEach((variable) => {
variables[variable.name] = variable.value ?? '';
@@ -24,14 +27,13 @@
<script lang="ts">
import { base } from '$app/paths';
import { AvatarGroup, Box, CardGrid, Heading } from '$lib/components';
import { AvatarGroup, Box, Heading } from '$lib/components';
import { app } from '$lib/stores/app';
import { wizard } from '$lib/stores/wizard';
import { repository, templateConfig, template as templateStore } from './store';
import { marketplace, type MarketplaceTemplate } from '$lib/stores/marketplace';
import { Button } from '$lib/elements/forms';
import { page } from '$app/stores';
import { baseRuntimesList } from '$routes/(console)/project-[project]/functions/store';
import { baseRuntimesList } from '$lib/stores/runtimes';
import { trackEvent } from '$lib/actions/analytics';
import type { Models } from '@appwrite.io/console';
import WizardCover from '$lib/layout/wizardCover.svelte';
@@ -41,14 +43,12 @@
import { tooltip } from '$lib/actions/tooltip';
import { isSelfHosted } from '$lib/system';
import { consoleVariables } from '$routes/(console)/store';
import { featuredTemplatesList, starterTemplate } from '$lib/stores/templates';
const isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
let hasInstallations: boolean;
let selectedRepository: string;
const quickStart = marketplace.find((template) => template.id === 'starter');
const templates = marketplace.filter((template) => template.id !== 'starter').slice(0, 2);
function connect(event: CustomEvent<Models.ProviderRepository>) {
trackEvent('click_connect_repository', {
from: 'cover'
@@ -61,55 +61,7 @@
<WizardCover>
<svelte:fragment slot="title">Create Function</svelte:fragment>
<div class="wizard-container container">
{#if isSelfHosted && !isVcsEnabled}
<div class="u-flex-vertical u-text-center">
<Heading tag="h5" size="5">Choose your source</Heading>
<p>
Connect your function to a Git repository or use a template to get started. You
can also create a function manually.
</p>
</div>
<CardGrid>
<Heading tag="h6" size="6">Create manually</Heading>
<p class="text">Create and deploy your function manually.</p>
<svelte:fragment slot="aside">
<div class="u-flex u-height-100-percent u-main-end u-cross-center">
<Button
secondary
on:click={() => {
trackEvent('click_create_function_manual', {
from: 'cover'
});
}}
on:click={() => wizard.start(CreateManual)}>Create function</Button>
</div>
</svelte:fragment>
</CardGrid>
{/if}
<div
class="git-container u-position-relative"
class:u-margin-block-start-24={isSelfHosted && !isVcsEnabled}>
{#if isSelfHosted && !isVcsEnabled}
<div
class="overlay u-flex-vertical u-position-absolute u-height-100-percent u-width-full-line u-z-index-1 card u-text-center">
<div
class="u-flex-vertical u-height-100-percent u-main-center u-cross-center u-gap-16">
<Heading size="7" tag="h6">
Configure your self-hosted instance to connect to Git
</Heading>
<p>
Connect your function to a Git repository or use a pre-made template<br />after
configuring your self-hosted instance. Learn more in our
<a
href="https://appwrite.io/docs/advanced/self-hosting/functions#git"
target="_blank"
rel="noopener noreferrer"
class="link">documentation</a
>.
</p>
</div>
</div>
{/if}
<div class="git-container u-position-relative">
<div class="grid-1-1 u-gap-24">
<div class="card u-cross-child-start u-height-100-percent">
<Heading size="6" tag="h6">Connect Git repository</Heading>
@@ -127,7 +79,30 @@
}}
on:connect={connect} />
</div>
{#if isSelfHosted && !isVcsEnabled}
<div
class="overlay u-flex-vertical u-position-absolute u-height-100-percent u-width-full-line u-z-index-1 u-text-center u-inset-0"
style="border-radius: var(--border-radius-medium)">
<div
class="u-flex-vertical u-height-100-percent u-main-center u-cross-center u-gap-16 u-padding-inline-24">
<Heading size="7" tag="h6" trimmed={false}>
Connect your self-hosted instance to Git
</Heading>
<p>
Configure your self-hosted instance to connect your function to
a Git repository.
<a
href="https://appwrite.io/docs/advanced/self-hosting/functions#git"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more</a
>.
</p>
</div>
</div>
{/if}
</div>
<div class="card u-height-100-percent">
<section class="common-section">
<Heading size="6" tag="h6">Quick start</Heading>
@@ -137,7 +112,7 @@
style:--grid-item-size="8rem"
style:--grid-item-size-small-screens="9rem"
style:--grid-gap=".5rem">
{#await $baseRuntimesList}
{#await Promise.all([$baseRuntimesList, $starterTemplate])}
{#each Array(6) as _i}
<li>
<button
@@ -151,7 +126,7 @@
</button>
</li>
{/each}
{:then response}
{:then [response, quickStart]}
{@const runtimes = new Map(
response.runtimes.map((r) => [r.$id, r])
)}
@@ -184,6 +159,9 @@
</div>
<div class="body-text-2">
{runtimeDetail.name}
{#if runtimeDetail.name.toLowerCase() === 'go'}
<span class="inline-tag">New</span>
{/if}
</div>
</button>
</li>
@@ -210,6 +188,13 @@
{/await}
</ul>
</section>
<Button
text
class="u-margin-block-start-24 u-margin-inline-start-auto"
href={`${base}/project-${$page.params.project}/functions/templates?useCase=Starter`}>
All starter templates <span class="icon-cheveron-right" />
</Button>
<div class="u-sep-block-start common-section" />
<section class="common-section">
<Heading size="6" tag="h6">Templates</Heading>
@@ -218,34 +203,56 @@
</p>
<ul class="clickable-list u-margin-block-start-16">
{#each templates as template}
<li class="clickable-list-item">
<button
type="button"
on:click={() => {
trackEvent('click_connect_template', {
from: 'cover',
template: template.id
});
}}
on:click={() => connectTemplate(template)}
class="clickable-list-button u-width-full-line u-flex u-gap-12">
<div
class="avatar is-size-small"
style:--p-text-size="1.25rem">
<span class={template.icon} />
</div>
<div class="u-flex u-flex-vertical u-gap-4">
<div class="body-text-2 u-bold u-trim">
{template.name}
{#await $featuredTemplatesList}
{#each Array(3) as _i}
<li>
<button
disabled
class="clickable-list-button u-width-full-line u-flex u-gap-12">
<div class="avatar is-size-small">
<div class="loader" />
</div>
<div class="u-trim-1 u-color-text-gray">
{template.tagline}
<div class="u-flex u-flex-vertical u-gap-4">
<div class="body-text-2 u-bold u-trim">
<div class="loader" />
</div>
<div class="u-trim-1 u-color-text-gray">
<div class="loader" />
</div>
</div>
</div>
</button>
</li>
{/each}
</button>
</li>
{/each}
{:then templatesListWithoutStarter}
{#each templatesListWithoutStarter.templates as template}
<li class="clickable-list-item">
<button
type="button"
on:click={() => {
trackEvent('click_connect_template', {
from: 'cover',
template: template.id
});
}}
on:click={() => connectTemplate(template)}
class="clickable-list-button u-width-full-line u-flex u-gap-12">
<div
class="avatar is-size-small"
style:--p-text-size="1.25rem">
<span class={template.icon} />
</div>
<div class="u-flex u-flex-vertical u-gap-4">
<div class="body-text-2 u-bold u-trim">
{template.name}
</div>
<div class="u-trim-1 u-color-text-gray">
{template.tagline}
</div>
</div>
</button>
</li>
{/each}
{/await}
</ul>
</section>
<Button
@@ -279,12 +286,17 @@
</div>
</WizardCover>
<style>
<style lang="scss">
.git-container .overlay {
background: linear-gradient(
0,
hsl(var(--p-card-bg-color)) 68.91%,
hsl(var(--p-card-bg-color) / 0.5) 95.8%
hsl(var(--p-card-bg-color) / 0.5) 92.8%
);
}
.inline-tag {
line-height: 140%;
font-weight: 500;
}
</style>
@@ -16,8 +16,8 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { base } from '$app/paths';
import { page } from '$app/stores';
import Details from './steps/details.svelte';
import Configuration from './steps/configuration.svelte';
import Details from './steps/manualDetails.svelte';
import Configuration from './steps/manualConfiguration.svelte';
import ExecuteAccess from './steps/executeAccess.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
+51 -40
View File
@@ -1,22 +1,28 @@
<script lang="ts">
import { ID, Runtime } from '@appwrite.io/console';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { goto } from '$app/navigation';
import { choices, installation, repository, template, templateConfig } from './store';
import {
choices,
installation,
repository,
template,
templateConfig,
templateStepsComponents
} from './store';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { base } from '$app/paths';
import { page } from '$app/stores';
import GitConfiguration from './steps/gitConfiguration.svelte';
import TemplateConfiguration from './steps/templateConfiguration.svelte';
import RepositoryBehaviour from './steps/repositoryBehaviour.svelte';
import CreateRepository from './steps/createRepository.svelte';
import TemplateVariables from './steps/templateVariables.svelte';
import { scopes } from '$lib/constants';
import { isValueOfStringEnum } from '$lib/helpers/types';
import TemplateConfiguration from './steps/templateConfiguration.svelte';
import TemplatePermissions from './steps/templatePermissions.svelte';
import TemplateVariables from './steps/templateVariables.svelte';
import TemplateDeployment from './steps/templateDeployment.svelte';
import CreateRepository from './steps/createRepository.svelte';
import GitConfiguration from './steps/gitConfiguration.svelte';
async function create() {
try {
@@ -26,22 +32,12 @@
const runtimeDetail = $template.runtimes.find(
(r) => r.name === $templateConfig.runtime
);
if ($templateConfig.appwriteApiKey) {
$templateConfig.variables['APPWRITE_API_KEY'] = $templateConfig.appwriteApiKey;
} else if ($templateConfig?.generateKey) {
const key = await sdk.forConsole.projects.createKey(
$page.params.project,
'Generated for Template',
scopes.map((scope) => scope.scope)
);
$templateConfig.variables['APPWRITE_API_KEY'] = key.secret;
}
const response = await sdk.forProject.functions.create(
$templateConfig.$id || ID.unique(),
$templateConfig.name,
$templateConfig.runtime,
$template.permissions || undefined,
$templateConfig?.execute ? $template.permissions || undefined : undefined,
$template.events || undefined,
$template.cron || undefined,
$template.timeout || undefined,
@@ -49,16 +45,20 @@
undefined,
runtimeDetail.entrypoint,
runtimeDetail.commands || undefined,
undefined,
$installation.$id,
$repository.id,
$choices.branch,
$choices.silentMode || undefined,
$choices.rootDir || undefined,
$template.providerRepositoryId,
$template.providerOwner,
runtimeDetail.providerRootDirectory,
$template.providerBranch
$templateConfig?.scopes?.length ? $templateConfig.scopes : undefined,
$templateConfig.repositoryBehaviour === 'manual' ? undefined : $installation.$id,
$templateConfig.repositoryBehaviour === 'manual' ? undefined : $repository.id,
$templateConfig.repositoryBehaviour === 'manual' ? undefined : $choices.branch,
$templateConfig.repositoryBehaviour === 'manual'
? undefined
: $choices.silentMode || undefined,
$templateConfig.repositoryBehaviour === 'manual'
? undefined
: $choices.rootDir || undefined,
$template.providerRepositoryId || undefined,
$template.providerOwner || undefined,
runtimeDetail.providerRootDirectory || undefined,
$template.providerVersion || undefined
);
if ($templateConfig.variables) {
@@ -75,7 +75,10 @@
type: 'success'
});
trackEvent(Submit.FunctionCreate, {
customId: !!response.$id
customId: !!response.$id,
runtime: response.runtime,
deployment_type: $templateConfig.repositoryBehaviour,
scopes: $templateConfig.scopes
});
resetState();
} catch (error) {
@@ -93,27 +96,35 @@
installation.set(null);
}
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
$templateStepsComponents.set(1, {
label: 'Configuration',
component: TemplateConfiguration
});
stepsComponents.set(2, {
$templateStepsComponents.set(2, {
label: 'Permissions',
component: TemplatePermissions
});
$templateStepsComponents.set(3, {
label: 'Variables',
component: TemplateVariables
component: TemplateVariables,
disabled: !$template?.variables?.length
});
stepsComponents.set(3, {
label: 'Connect',
component: RepositoryBehaviour
$templateStepsComponents.set(4, {
label: 'Deployment',
component: TemplateDeployment
});
stepsComponents.set(4, {
$templateStepsComponents.set(5, {
label: 'Repository',
component: CreateRepository
});
stepsComponents.set(5, {
$templateStepsComponents.set(6, {
label: 'Branch',
component: GitConfiguration
});
</script>
<Wizard title="Create Function" steps={stepsComponents} on:finish={create} on:exit={resetState} />
<Wizard
title="Create Function"
steps={$templateStepsComponents}
on:finish={create}
on:exit={resetState} />
@@ -7,7 +7,7 @@
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { choices, createFunction, installation, repository } from '../store';
import { runtimesList } from '$routes/(console)/project-[project]/functions/store';
import { runtimesList } from '$lib/stores/runtimes';
let showCustomId = false;
@@ -5,7 +5,7 @@
import { WizardStep } from '$lib/layout';
import { onMount } from 'svelte';
import { createFunction } from '../store';
import { runtimesList } from '$routes/(console)/project-[project]/functions/store';
import { runtimesList } from '$lib/stores/runtimes';
let showCustomId = false;
@@ -1,41 +0,0 @@
<script lang="ts">
import { LabelCard } from '$lib/components';
import { WizardStep } from '$lib/layout';
import { app } from '$lib/stores/app';
import { templateConfig } from '../store';
async function beforeSubmit() {
if (!$templateConfig.repositoryBehaviour) {
throw new Error('Please select repository behaviour.');
}
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Connect</svelte:fragment>
<svelte:fragment slot="subtitle">
Connect function to a new repository or to an existing one within a selected Git
organization.
</svelte:fragment>
<ul class="u-flex u-flex-vertical u-gap-16">
<LabelCard
name="behaviour"
value="new"
backgroundColor={$app.themeInUse === 'light' ? 'var(--color-neutral-5)' : null}
backgroundColorHover={$app.themeInUse === 'light' ? 'var(--color-neutral-10)' : null}
bind:group={$templateConfig.repositoryBehaviour}>
<svelte:fragment slot="title">Create a new repository</svelte:fragment>
Clone the template and create a new repository in your selected organization.
</LabelCard>
<LabelCard
name="behaviour"
value="existing"
backgroundColor={$app.themeInUse === 'light' ? 'var(--color-neutral-5)' : null}
backgroundColorHover={$app.themeInUse === 'light' ? 'var(--color-neutral-10)' : null}
bind:group={$templateConfig.repositoryBehaviour}>
<svelte:fragment slot="title">Add to existing repository</svelte:fragment>
Clone the template to an existing repository in your selected organization.
</LabelCard>
</ul>
</WizardStep>
@@ -3,7 +3,7 @@
import { Pill } from '$lib/elements';
import { FormList, InputSelect, InputText } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { runtimesList } from '$routes/(console)/project-[project]/functions/store';
import { runtimesList } from '$lib/stores/runtimes';
import { template, templateConfig } from '../store';
let showCustomId = false;
@@ -0,0 +1,64 @@
<script lang="ts">
import { LabelCard } from '$lib/components';
import { FormList } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { consoleVariables } from '$routes/(console)/store';
import { onMount } from 'svelte';
import { templateConfig, templateStepsComponents } from '../store';
const isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true;
onMount(() => {
if (!isVcsEnabled) {
$templateConfig.repositoryBehaviour = 'manual';
}
});
async function beforeSubmit() {
if (!$templateConfig.repositoryBehaviour) {
throw new Error('Please select repository behaviour.');
}
}
$: if ($templateConfig.repositoryBehaviour === 'manual') {
$templateStepsComponents.get(5).disabled = true;
$templateStepsComponents.get(6).disabled = true;
$templateStepsComponents = $templateStepsComponents;
} else {
$templateStepsComponents.get(5).disabled = false;
$templateStepsComponents.get(6).disabled = false;
$templateStepsComponents = $templateStepsComponents;
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Deployment</svelte:fragment>
<h3>Connect with Git <span class="inline-code">Recommended</span></h3>
<FormList gap={16} class="u-margin-block-start-8">
<LabelCard
name="behaviour"
value="new"
bind:group={$templateConfig.repositoryBehaviour}
disabled={!isVcsEnabled}>
<svelte:fragment slot="title">Create a new repository</svelte:fragment>
Clone the template to a newly created repository in your organization.
</LabelCard>
<LabelCard
name="behaviour"
value="existing"
bind:group={$templateConfig.repositoryBehaviour}
disabled={!isVcsEnabled}>
<svelte:fragment slot="title">Add to existing repository</svelte:fragment>
Clone the template to an existing repository in your organization.
</LabelCard>
</FormList>
<h3 class="u-margin-block-start-24">Quick start</h3>
<ul class="u-margin-block-start-8">
<LabelCard name="behaviour" value="manual" bind:group={$templateConfig.repositoryBehaviour}>
<svelte:fragment slot="title">Connect later</svelte:fragment>
Deploy now and continue development via CLI, or connect Git from your function settings.
</LabelCard>
</ul>
</WizardStep>
@@ -0,0 +1,88 @@
<script lang="ts">
import { scopes } from '$lib/constants';
import { FormList, InputChoice } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { onMount } from 'svelte';
import { template, templateConfig } from '../store';
import { tooltip } from '$lib/actions/tooltip';
let templateScopes = [];
onMount(() => {
$templateConfig.execute = $template.permissions.includes('any');
templateScopes = scopes
.filter((scope) => $template.scopes.includes(scope.scope))
.map((scope) => ({
...scope,
active: true
}));
});
$: $templateConfig.scopes = templateScopes
?.filter((scope) => scope.active)
?.map((scope) => scope.scope);
</script>
<WizardStep>
<svelte:fragment slot="title">Permissions</svelte:fragment>
<svelte:fragment slot="subtitle">
Enable recommended scopes and execute access for when your function is deployed.
</svelte:fragment>
<h3 class="label">Execute permissions</h3>
<FormList class="u-margin-block-start-16">
<div class="user-profile">
<span class="avatar" style:--p-text-size="1rem">
<span class="icon-lock-open" />
</span>
<span class="user-profile-info u-flex u-main-space-between u-gap-16">
<div>
<p class="name u-bold">
Public (anyone can execute) <span
class="icon-info"
use:tooltip={{
content:
'You can further customize execute permissions in your function settings.'
}} />
</p>
<p class="text u-margin-block-start-4">
This could include unauthorized users and search engines.
</p>
</div>
<InputChoice
type="switchbox"
id="execute"
label="Execute"
showLabel={false}
bind:value={$templateConfig.execute}></InputChoice>
</span>
</div>
</FormList>
{#if templateScopes.length > 0}
<h3 class="label u-margin-block-start-48">Function scopes</h3>
<FormList class="u-margin-block-start-16">
{#each templateScopes as scope, i}
<div class="user-profile">
<span class="avatar" style:--p-text-size="1rem">
<span class={`icon-${scope.icon}`} />
</span>
<span class="user-profile-info u-flex u-main-space-between u-gap-16">
<div>
<p class="name u-bold">{scope.scope}</p>
<p class="text u-margin-block-start-4">{scope.description}</p>
</div>
<InputChoice
type="switchbox"
id={scope.scope}
label={scope.scope}
showLabel={false}
bind:value={scope.active}>
</InputChoice>
</span>
</div>
{#if i < templateScopes.length - 1}
<div class="with-separators"></div>
{/if}
{/each}
</FormList>
{/if}
</WizardStep>
@@ -12,7 +12,6 @@
InputNumber
} from '$lib/elements/forms';
import { Card, Collapsible, CollapsibleItem } from '$lib/components';
import AppwriteVariable from '../components/appwriteVariable.svelte';
import type { SvelteComponent } from 'svelte';
async function beforeSubmit() {
@@ -20,10 +19,6 @@
if (!variable.required) {
continue;
}
if (variable.name === 'APPWRITE_API_KEY') {
if ($templateConfig.appwriteApiKey || $templateConfig.generateKey) continue;
else throw new Error(`Please set ${variable.name} variable or generate it.`);
}
if (!$templateConfig.variables[variable.name]) {
throw new Error(`Please set ${variable.name} variable.`);
}
@@ -33,9 +28,7 @@
$: requiredVariables = $template?.variables?.filter((v) => v.required);
$: optionalVariables = $template?.variables?.filter((v) => !v.required);
function selectComponent(
variableType: 'password' | 'text' | 'number' | 'email' | 'url' | 'phone'
): typeof SvelteComponent<unknown> {
function selectComponent(variableType: string): typeof SvelteComponent<unknown> {
switch (variableType) {
case 'password':
return InputPassword;
@@ -72,25 +65,21 @@
<FormList>
{#each requiredVariables as variable}
{#if variable.name === 'APPWRITE_API_KEY'}
<AppwriteVariable appwriteVariable={variable} />
{:else}
<div>
<svelte:component
this={selectComponent(variable.type)}
id={variable.name}
label={variable.name}
placeholder={variable.placeholder ?? 'Enter value'}
required={variable.required}
autocomplete={false}
minlength={variable.type === 'password' ? 0 : null}
showPasswordButton={variable.type === 'password'}
bind:value={$templateConfig.variables[variable.name]} />
<Helper type="neutral">
{@html variable.description}
</Helper>
</div>
{/if}
<div>
<svelte:component
this={selectComponent(variable.type)}
id={variable.name}
label={variable.name}
placeholder={variable.placeholder ?? 'Enter value'}
required={variable.required}
autocomplete={false}
minlength={variable.type === 'password' ? 0 : null}
showPasswordButton={variable.type === 'password'}
bind:value={$templateConfig.variables[variable.name]} />
<Helper type="neutral">
{@html variable.description}
</Helper>
</div>
{/each}
</FormList>
</CollapsibleItem>
@@ -107,25 +96,21 @@
<FormList>
{#each optionalVariables as variable}
{#if variable.name === 'APPWRITE_API_KEY'}
<AppwriteVariable appwriteVariable={variable} />
{:else}
<div>
<svelte:component
this={selectComponent(variable.type)}
id={variable.name}
label={variable.name}
placeholder={variable.placeholder ?? 'Enter value'}
required={variable.required}
autocomplete={false}
showPasswordButton={variable.type === 'password'}
bind:value={$templateConfig.variables[variable.name]} />
<div>
<svelte:component
this={selectComponent(variable.type)}
id={variable.name}
label={variable.name}
placeholder={variable.placeholder ?? 'Enter value'}
required={variable.required}
autocomplete={false}
showPasswordButton={variable.type === 'password'}
bind:value={$templateConfig.variables[variable.name]} />
<Helper type="neutral">
{@html variable.description}
</Helper>
</div>
{/if}
<Helper type="neutral">
{@html variable.description}
</Helper>
</div>
{/each}
</FormList>
</CollapsibleItem>
+10 -8
View File
@@ -1,20 +1,20 @@
import { page } from '$app/stores';
import type { MarketplaceTemplate } from '$lib/stores/marketplace';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import type { Models } from '@appwrite.io/console';
import { derived, writable } from 'svelte/store';
import { derived, writable, type Writable } from 'svelte/store';
export const template = writable<MarketplaceTemplate>();
export const template = writable<Models.TemplateFunction>();
export const templateConfig = writable<{
$id: string;
name: string;
runtime: string;
variables: { [key: string]: unknown };
repositoryBehaviour: 'new' | 'existing';
repositoryName: string;
repositoryPrivate: boolean;
repositoryBehaviour: 'new' | 'existing' | 'manual';
repositoryName?: string;
repositoryPrivate?: boolean;
repositoryId: string;
appwriteApiKey?: string;
generateKey?: boolean;
execute?: boolean;
scopes?: string[];
}>();
export const repository = writable<Models.ProviderRepository>();
export const installation = writable<Models.Installation>();
@@ -59,3 +59,5 @@ function createFunctionStore() {
export const createFunction = createFunctionStore();
export const createFunctionDeployment = writable<FileList>();
export const templateStepsComponents: Writable<WizardStepsType> = writable(new Map());
+7 -1
View File
@@ -1,7 +1,13 @@
import type { PageLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
export const load: PageLoad = async () => {
export const load: PageLoad = async ({ parent }) => {
const { mfaRequired } = await parent();
if (!mfaRequired) {
redirect(303, base);
}
return {
factors: await sdk.forConsole.account.listMfaFactors()
};
@@ -119,8 +119,8 @@
$provider.subdomain,
$provider.region,
$provider.adminSecret,
$provider.database,
$provider.username,
$provider.database || $provider.subdomain,
$provider.username || 'postgres',
$provider.password
);
}
@@ -186,7 +186,7 @@
</div>
</Box>
{#if report && !isVersionAtLeast(version, '1.4.0')}
{#if report && !isVersionAtLeast(version, '1.4.0') && $provider.provider === 'appwrite'}
<div class="u-margin-block-start-24">
<Alert
type="warning"
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { InputSelect } from '$lib/elements/forms';
import { FormList, InputSelect } from '$lib/elements/forms';
import InputText from '$lib/elements/forms/inputText.svelte';
import { WizardStep } from '$lib/layout';
import { Query, type Models, ID } from '@appwrite.io/console';
@@ -44,7 +44,7 @@
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Project</svelte:fragment>
<div class="u-flex u-flex-vertical u-gap-24">
<FormList>
{#if organizations.length > 1}
<InputSelect
id="organization"
@@ -107,7 +107,7 @@
{/if}
</div>
{/if}
</div>
</FormList>
</WizardStep>
<style lang="scss">
@@ -4,20 +4,17 @@
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorType } from '@appwrite.io/console';
export let showDelete = false;
let code: string;
let error: string;
async function deleteAuthenticator() {
try {
await sdk.forConsole.account.deleteMfaAuthenticator(AuthenticatorType.Totp, code);
await sdk.forConsole.account.deleteMfaAuthenticator(AuthenticatorType.Totp);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showDelete = false;
@@ -33,7 +30,6 @@
}
$: if (showDelete) {
code = '';
error = '';
}
</script>
@@ -46,10 +42,10 @@
state="warning"
bind:error
headerDivider={false}>
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
<FormList>
<InputDigits autofocus required bind:value={code} autoSubmit={false} />
</FormList>
<p>
Are you sure you want to delete this authentication method? You will no longer be able to
use this method to authenticate your account.
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
+2 -2
View File
@@ -16,7 +16,7 @@
let step = 1;
let error = '';
async function addAuthenticator(): Promise<URL> {
async function addAuthenticator(): Promise<string> {
type = await sdk.forConsole.account.createMfaAuthenticator(AuthenticatorType.Totp);
trackEvent(Submit.AccountAuthenticatorCreate);
@@ -58,7 +58,7 @@
style:background-repeat="no-repeat"
style:background-position="center"
style:background-size="contain">
<img alt="MFA QR Code" class="code" src={qr.toString()} />
<img alt="MFA QR Code" class="code" src={qr} />
</div>
<span class="with-separators eyebrow-heading-3">or</span>
@@ -69,6 +69,7 @@
let coupon: string;
let couponData = data?.couponData;
let campaign = campaigns.get(data?.couponData?.campaign ?? data?.campaign);
let billingPlan = BillingPlan.PRO;
onMount(async () => {
await loadPaymentMethods();
@@ -79,6 +80,9 @@
selectedOrgId = $page.url.searchParams.get('org');
canSelectOrg = false;
}
if (campaign?.plan) {
billingPlan = campaign.plan;
}
});
async function loadPaymentMethods() {
@@ -100,15 +104,15 @@
org = await sdk.forConsole.billing.createOrganization(
newOrgId,
name,
BillingPlan.PRO,
billingPlan,
paymentMethodId
);
}
// Upgrade existing org
else if (selectedOrg?.billingPlan !== BillingPlan.PRO) {
else if (selectedOrg?.billingPlan === BillingPlan.FREE) {
org = await sdk.forConsole.billing.updatePlan(
selectedOrg.$id,
BillingPlan.PRO,
billingPlan,
paymentMethodId,
null
);
@@ -267,7 +271,7 @@
</div>
</div>
{/if}
{#if selectedOrg?.billingPlan === BillingPlan.PRO}
{#if selectedOrg?.billingPlan !== BillingPlan.FREE}
<section
class="card u-margin-block-start-24"
style:--p-card-padding="1.5rem"
@@ -276,7 +280,7 @@
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
<p class="text u-margin-block-start-12">
Credits will automatically be applied to your next invoice on <b
>{toLocaleDate(selectedOrg.billingNextInvoiceDate)}.</b>
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}.</b>
</p>
{:else}
<p class="text">Add a coupon code to apply credits to your organization.</p>
@@ -3,23 +3,22 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { LabelCard } from '$lib/components';
import {
EstimatedTotalBox,
PlanComparisonBox,
PlanSelection,
SelectPaymentMethod
} from '$lib/components/billing';
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputTags, InputText, Label } from '$lib/elements/forms';
import { formatCurrency } from '$lib/helpers/numbers';
import {
WizardSecondaryContainer,
WizardSecondaryContent,
WizardSecondaryFooter
} from '$lib/layout';
import type { Coupon, PaymentList } from '$lib/sdk/billing';
import { plansInfo, tierFree, tierPro, tierToPlan } from '$lib/stores/billing';
import { tierToPlan } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { organizationList, type Organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
@@ -27,7 +26,7 @@
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
$: anyOrgFree = $organizationList.teams?.find(
$: anyOrgFree = $organizationList.teams?.some(
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
);
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
@@ -174,9 +173,7 @@
}
}
$: freePlan = $plansInfo.get(BillingPlan.FREE);
$: proPlan = $plansInfo.get(BillingPlan.PRO);
$: if (billingPlan === BillingPlan.PRO) {
$: if (billingPlan !== BillingPlan.FREE) {
loadPaymentMethods();
}
</script>
@@ -202,53 +199,9 @@
For more details on our plans, visit our
<Button href="https://appwrite.io/pricing" external link>pricing page</Button>.
</p>
<ul
class="u-flex u-gap-16 u-margin-block-start-8"
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
<li class="u-flex-basis-50-percent">
<LabelCard
name="plan"
bind:group={billingPlan}
value="tier-0"
disabled={!!anyOrgFree}
tooltipShow={!!anyOrgFree}
tooltipText="You are limited to 1 Free organization per account.">
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={disabled}>
<h4 class="body-text-2 u-bold">
{tierFree.name}
</h4>
<p class="u-color-text-gray u-small">{tierFree.description}</p>
<p>
{formatCurrency(freePlan?.price ?? 0)}
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
<li class="u-flex-basis-50-percent">
<LabelCard name="plan" bind:group={billingPlan} value="tier-1">
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{tierPro.name}
</h4>
<p class="u-color-text-gray u-small">
{tierPro.description}
</p>
<p>
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
</ul>
{#if billingPlan === BillingPlan.PRO}
<FormList class="u-margin-block-start-16">
<PlanSelection bind:billingPlan {anyOrgFree} isNewOrg />
{#if billingPlan !== BillingPlan.FREE}
<FormList class="u-margin-block-start-24">
<InputTags
bind:tags={collaborators}
label="Invite members by email"
@@ -32,6 +32,10 @@
{
value: BillingPlan.PRO,
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons`
},
{
value: BillingPlan.SCALE,
label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
}
]
: [];
@@ -111,8 +111,8 @@
{/if}
{#if $organization?.billingPlanDowngrade}
<Alert type="info" class="common-section">
Your organization will change to a {tierToPlan(BillingPlan.FREE).name} plan once your current
billing cycle ends and your invoice is paid on {toLocaleDate(
Your organization will change to a {tierToPlan($organization?.billingPlanDowngrade)
.name} plan once your current billing cycle ends and your invoice is paid on {toLocaleDate(
$organization.billingNextInvoiceDate
)}.
</Alert>
@@ -1,15 +1,15 @@
<script lang="ts">
import { Alert, CardGrid, Empty, Heading, PaginationInline } from '$lib/components';
import {
Table,
TableBody,
TableCellHead,
TableCellText,
TableHeader,
TableRow
TableRow,
TableScroll
} from '$lib/elements/table';
import { toLocaleDate } from '$lib/helpers/date';
import type { Credit, CreditList } from '$lib/sdk/billing';
import type { CreditList } from '$lib/sdk/billing';
import { organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
@@ -25,6 +25,7 @@
let offset = 0;
let creditList: CreditList = {
available: 0,
credits: [],
total: 0
};
@@ -56,9 +57,6 @@
request();
}
$: balance =
creditList?.credits?.reduce((acc: number, curr: Credit) => acc + curr.credits, 0) ?? 0;
$: {
if (reloadOnWizardClose && !$wizard.show) {
request();
@@ -86,8 +84,8 @@
{:else}
<div class="u-flex u-cross-center u-main-space-between">
<div class="u-flex u-gap-8 u-cross-center">
<h4 class="body-text-1 u-bold">Credit balance</h4>
<span class="inline-tag">{formatCurrency(balance)}</span>
<h4 class="body-text-1 u-bold">Balance</h4>
<span class="inline-tag">{formatCurrency(creditList.available)}</span>
</div>
{#if creditList?.total}
<Button secondary on:click={handleCredits}>
@@ -97,30 +95,34 @@
{/if}
</div>
{#if creditList?.total}
<Table noStyles noMargin>
<TableScroll noStyles noMargin class="u-margin-block-start-16">
<TableHeader>
<TableCellHead>Date Added</TableCellHead>
<TableCellHead>Expiry Date</TableCellHead>
<TableCellHead>Amount</TableCellHead>
<TableCellHead>Code</TableCellHead>
<TableCellHead>Total</TableCellHead>
<TableCellHead>Remaining</TableCellHead>
<TableCellHead>Expires at</TableCellHead>
</TableHeader>
<TableBody>
{#each creditList.credits as credit}
<TableRow>
<TableCellText title="date added">
{toLocaleDate(credit.$createdAt)}
<TableCellText title="code">
{credit?.couponId ?? '-'}
</TableCellText>
<TableCellText title="total">
{formatCurrency(credit.total)}
</TableCellText>
<TableCellText title="remaining">
{formatCurrency(credit.credits)}
</TableCellText>
<TableCellText title="expiry date">
{toLocaleDate(credit.expiration)}
</TableCellText>
<TableCellText title="amount">
{formatCurrency(credit.total)}
</TableCellText>
</TableRow>
{/each}
</TableBody>
</Table>
</TableScroll>
<div class="u-flex u-main-space-between">
<p class="text">Total results: {creditList?.total}</p>
<p class="text">Total credits: {creditList?.total}</p>
<PaginationInline {limit} bind:offset sum={creditList?.total} hidePages />
</div>
{:else}
@@ -125,7 +125,7 @@
{/if}
</TableCell>
<TableCellText title="due">
{formatCurrency(invoice.amount)}
{formatCurrency(invoice.grossAmount)}
</TableCellText>
<TableCell showOverflow right>
<DropList
@@ -58,7 +58,7 @@
{isTrial ? formatCurrency(0) : formatCurrency(currentPlan?.price)}
</div>
</CollapsibleItem>
{#if $organization?.billingPlan !== BillingPlan.FREE && extraUsage}
{#if $organization?.billingPlan !== BillingPlan.FREE && extraUsage > 0}
<CollapsibleItem isInfo gap={8}>
<svelte:fragment slot="beforetitle">
<span class="body-text-2"><b>Add-ons</b></span><span class="inline-tag"
@@ -19,6 +19,7 @@
import { paymentMethods } from '$lib/stores/billing';
import { onMount } from 'svelte';
import { getApiEndpoint, sdk } from '$lib/stores/sdk';
import { formatCurrency } from '$lib/helpers/numbers';
export let show = false;
export let invoice: Invoice;
@@ -121,9 +122,9 @@
title="Retry payment">
<!-- TODO: format currency -->
<p class="text">
Your payment of <span class="inline-tag">${invoice.amount}</span> due on {toLocaleDate(
invoice.dueAt
)} has failed. Retry your payment to avoid service interruptions with your projects.
Your payment of <span class="inline-tag">${formatCurrency(invoice.grossAmount)}</span> due
on {toLocaleDate(invoice.dueAt)} has failed. Retry your payment to avoid service interruptions
with your projects.
</p>
<Button
@@ -3,13 +3,14 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Alert, LabelCard } from '$lib/components';
import { Alert } from '$lib/components';
import {
EstimatedTotalBox,
PlanComparisonBox,
SelectPaymentMethod
} from '$lib/components/billing';
import PlanExcess from '$lib/components/billing/planExcess.svelte';
import PlanSelection from '$lib/components/billing/planSelection.svelte';
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants';
import {
@@ -21,14 +22,14 @@
InputTextarea,
Label
} from '$lib/elements/forms';
import { formatCurrency } from '$lib/helpers/numbers';
import { formatCurrency } from '$lib/helpers/numbers.js';
import {
WizardSecondaryContainer,
WizardSecondaryContent,
WizardSecondaryFooter
} from '$lib/layout';
import { type Coupon, type PaymentList } from '$lib/sdk/billing';
import { plansInfo, tierFree, tierPro, tierToPlan, type Tier } from '$lib/stores/billing';
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { organization, organizationList, type Organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
@@ -92,7 +93,11 @@
billingPlan = plan as BillingPlan;
}
}
billingPlan = BillingPlan.PRO;
if ($organization?.billingPlan === BillingPlan.SCALE) {
billingPlan = BillingPlan.SCALE;
} else {
billingPlan = BillingPlan.PRO;
}
});
async function loadPaymentMethods() {
@@ -105,9 +110,9 @@
}
async function handleSubmit() {
if (billingPlan === BillingPlan.FREE) {
if (isDowngrade) {
await downgrade();
} else {
} else if (isUpgrade) {
await upgrade();
}
}
@@ -237,9 +242,7 @@
$: isUpgrade = billingPlan > $organization.billingPlan;
$: isDowngrade = billingPlan < $organization.billingPlan;
$: freePlan = $plansInfo.get(BillingPlan.FREE);
$: proPlan = $plansInfo.get(BillingPlan.PRO);
$: if (billingPlan === BillingPlan.PRO) {
$: if (billingPlan !== BillingPlan.FREE) {
loadPaymentMethods();
}
$: isButtonDisabled = $organization.billingPlan === billingPlan;
@@ -258,71 +261,37 @@
For more details on our plans, visit our
<Button href="https://appwrite.io/pricing" external link>pricing page</Button>.
</p>
{#if anyOrgFree && billingPlan === BillingPlan.PRO}
<Alert type="warning" class="u-margin-block-16">
You are limited to one {tierToPlan(BillingPlan.FREE).name} organization per account.
Consider upgrading or deleting <Button
link
href={`${base}/organization-${anyOrgFree.$id}`}>{anyOrgFree.name}</Button
>.
</Alert>
{/if}
<ul
class="u-flex u-gap-16 u-margin-block-start-8"
class:u-margin-block-start-16={anyOrgFree && billingPlan === BillingPlan.PRO}
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
<li class="u-flex-basis-50-percent">
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={!!anyOrgFree}
value="tier-0"
tooltipShow={!!anyOrgFree}
tooltipText="You are limited to 1 Free organization per account.">
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={disabled}>
<h4 class="body-text-2 u-bold">
{tierFree.name}
{#if $organization.billingPlan === BillingPlan.FREE}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-gray u-small">{tierFree.description}</p>
<p>
{formatCurrency(freePlan?.price ?? 0)}
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
<PlanSelection
bind:billingPlan
anyOrgFree={!!anyOrgFree}
class={anyOrgFree && billingPlan !== BillingPlan.FREE
? 'u-margin-block-start-16'
: ''} />
<li class="u-flex-basis-50-percent">
<LabelCard name="plan" bind:group={billingPlan} value="tier-1">
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{tierPro.name}
{#if $organization.billingPlan === BillingPlan.PRO}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-gray u-small">
{tierPro.description}
</p>
<p>
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
</ul>
{#if isDowngrade}
<PlanExcess tier={BillingPlan.FREE} class="u-margin-block-start-24" />
{#if billingPlan === BillingPlan.FREE}
<PlanExcess
tier={BillingPlan.FREE}
class="u-margin-block-start-24"
members={data?.members?.total ?? 0} />
{:else if billingPlan === BillingPlan.PRO && $organization.billingPlan === BillingPlan.SCALE}
{@const extraMembers = collaborators?.length ?? 0}
<Alert type="error" class="u-margin-block-start-24">
<svelte:fragment slot="title">
Your monthly payments will be adjusted for the Pro plan
</svelte:fragment>
After switching plans,
<b
>you will be charged {formatCurrency(
extraMembers *
($plansInfo?.get(billingPlan)?.addons?.member?.price ?? 0)
)} monthly for {extraMembers} team members.</b> This will be reflected in
your next invoice.
</Alert>
{/if}
{/if}
{#if billingPlan === BillingPlan.PRO && $organization.billingPlan !== BillingPlan.PRO}
<!-- Show email input if upgrading from free plan -->
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan === BillingPlan.FREE}
<FormList class="u-margin-block-start-16">
<InputTags
bind:tags={collaborators}
@@ -344,7 +313,7 @@
</Button>
{/if}
{/if}
{#if !isUpgrade && billingPlan === BillingPlan.FREE && $organization.billingPlan !== BillingPlan.FREE}
{#if isDowngrade}
<FormList class="u-margin-block-start-24">
<InputSelect
id="reason"
@@ -362,12 +331,13 @@
{/if}
</Form>
<svelte:fragment slot="aside">
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== BillingPlan.PRO}
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== billingPlan}
<EstimatedTotalBox
{billingPlan}
{collaborators}
bind:couponData
bind:billingBudget />
bind:billingBudget
{isDowngrade} />
{:else}
<PlanComparisonBox downgrade={isDowngrade} />
{/if}
@@ -3,8 +3,8 @@ import type { PageLoad } from './$types';
export const load: PageLoad = async ({ depends, parent }) => {
const { members } = await parent();
depends(Dependencies.ORGANIZATION);
depends(Dependencies.ORGANIZATION);
return {
members
};
@@ -59,14 +59,12 @@
<Modal title="Invite member" {error} size="big" bind:show={showCreate} onSubmit={create}>
{#if isCloud}
<Alert type="info">
{#if $organization?.billingPlan === BillingPlan.SCALE}
You can add unlimited organization members on the {plan.name} plan at no cost.
{:else if $organization?.billingPlan === BillingPlan.PRO}
{#if $organization?.billingPlan === BillingPlan.PRO}
<Alert type="info">
You can add unlimited organization members on the {plan.name} plan for
<b>{formatCurrency(plan.addons.member.price)} each per billing period</b>.
{/if}
</Alert>
</Alert>
{/if}
{/if}
<FormList>
<InputEmail
@@ -84,6 +82,6 @@
</FormList>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (showCreate = false)}>Cancel</Button>
<Button submit>Send invite</Button>
<Button submit submissionLoader>Send invite</Button>
</svelte:fragment>
</Modal>
@@ -21,7 +21,7 @@
export let data;
const tier = data?.currentInvoice?.tier ?? $organization?.billingPlan;
const tier = data?.currentInvoice?.plan ?? $organization?.billingPlan;
const plan = tierToPlan(tier).name;
// let invoice = null;
@@ -46,7 +46,7 @@
<span
class="icon-info"
use:tooltip={{
content: `You can add unlimited organization members on the ${tierToPlan($organization.billingPlan).name} plan for ${formatCurrency(plan.addons.member.price)} each per billing period.`
content: `You can add unlimited organization members on the ${tierToPlan($organization.billingPlan).name} plan ${$organization.billingPlan === BillingPlan.PRO ? `for ${formatCurrency(plan.addons.member.price)} each per billing period.` : '.'}`
}}></span>
</p>
</div>
@@ -53,8 +53,8 @@
</p>
<InputSwitch id="state" bind:value={enabled} label={enabled ? 'Enabled' : 'Disabled'} />
<InputText
id="bundleID"
label="Bundle ID"
id="servicesID"
label="Services ID"
autofocus={true}
placeholder="com.company.appname"
bind:value={appId} />
@@ -4,6 +4,7 @@
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
import UpdatePersonalDataCheck from './updatePersonalDataCheck.svelte';
import UpdateSessionAlerts from './updateSessionAlerts.svelte';
import UpdateSessionLength from './updateSessionLength.svelte';
import UpdateSessionsLimit from './updateSessionsLimit.svelte';
import UpdateUsersLimit from './updateUsersLimit.svelte';
@@ -11,10 +12,11 @@
<Container>
<UpdateUsersLimit />
<UpdateMockNumbers />
<UpdateSessionLength />
<UpdateSessionsLimit />
<UpdatePasswordHistory />
<UpdatePasswordDictionary />
<UpdatePersonalDataCheck />
<UpdateSessionAlerts />
<UpdateMockNumbers />
</Container>
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -1,22 +1,40 @@
<script lang="ts">
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { InputPhone, InputText } from '$lib/elements/forms';
import { InputPhone, InputOTP } from '$lib/elements/forms';
import { Button, Form, FormItem, FormItemPart } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import { isCloud, isSelfHosted } from '$lib/system';
import MockNumbersLight from './mock-numbers-light.png';
import MockNumbersDark from './mock-numbers-dark.png';
import EmptyCardImageCloud from '$lib/components/emptyCardImageCloud.svelte';
import { app } from '$lib/stores/app';
import Empty from '$lib/components/empty.svelte';
import type { Models } from '@appwrite.io/console';
import { tooltip } from '$lib/actions/tooltip';
let numbers: Models.MockNumber[] = $project?.authMockNumbers ?? [];
let initialNumbers = [];
let projectId: string = $project.$id;
$: initialNumbers = $project?.authMockNumbers?.map((num) => ({ ...num })) ?? [];
$: submitDisabled = JSON.stringify(numbers) === JSON.stringify(initialNumbers);
$: isSubmitDisabled = JSON.stringify(numbers) === JSON.stringify(initialNumbers);
let isComponentDisabled: boolean =
isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE);
let emptyStateTitle: string = isSelfHosted
? 'Available on Appwrite Cloud'
: 'Upgrade to add mock phone numbers';
let emptyStateDescription: string = isSelfHosted
? 'Sign up to Cloud to add mock phone numbers to your projects.'
: 'Upgrade to a Pro plan to add mock phone numbers to your project.';
let cta: string = isSelfHosted ? 'Sign up' : 'Upgrade plan';
async function updateMockNumbers() {
try {
@@ -24,7 +42,7 @@
await invalidate(Dependencies.PROJECT);
addNotification({
type: 'success',
message: 'Updated mock phone numbers successfully'
message: 'Mock phone numbers have been updated'
});
trackEvent(Submit.AuthMockNumbersUpdate);
} catch (error) {
@@ -48,44 +66,138 @@
numbers.splice(index, 1);
numbers = numbers;
}
function generateNumber(): string {
const areaCode = Math.floor(Math.random() * 800) + 200;
const lineNumber = Math.floor(Math.random() * 10000)
.toString()
.padStart(4, '0');
return `+1${areaCode}555${lineNumber}`;
}
function generateOTP(): string {
return Math.floor(100000 + Math.random() * 900000) + '';
}
</script>
<Form onSubmit={updateMockNumbers}>
<CardGrid>
<Heading tag="h6" size="7" id="variables">Mock Phone Numbers</Heading>
<CardGrid hideFooter={isComponentDisabled}>
<Heading tag="h6" size="7" id="variables">Mock phone numbers</Heading>
<p>
Generate <b>fictional</b> numbers to simulate phone verification while testing demo accounts.
A maximum of 10 phone numbers can be generated.
Generate <b>fictional</b> numbers to simulate phone verification when testing demo
accounts for submitting your application to the App Store or Google Play.
<a
href="https://appwrite.io/docs/products/auth/security#mock-numbers"
target="_blank"
class="u-underline"
rel="noopener noreferrer">
Learn more</a>
</p>
<svelte:fragment slot="aside">
{#if numbers?.length > 0}
<ul class="form-list">
{#if isComponentDisabled}
<EmptyCardImageCloud source="email_signature_card" noAspectRatio>
<svelte:fragment slot="image">
<div class=" is-only-mobile u-width-full-line u-height-100-percent">
{#if $app.themeInUse === 'dark'}
<img
src={MockNumbersDark}
class="u-image-object-fit-cover u-only-dark u-width-full-line u-height-100-percent"
alt="Mock Numbers Example" />
{:else}
<img
src={MockNumbersLight}
class="u-image-object-fit-cover u-only-light u-width-full-line u-height-100-percent"
alt="Mock Numbers Example" />
{/if}
</div>
<div class="is-not-mobile u-width-full-line u-height-100-percent">
{#if $app.themeInUse === 'dark'}
<img
src={MockNumbersDark}
width="266"
height="171"
class="u-image-object-fit-contain u-block u-only-dark u-width-full-line u-height-100-percent"
style:object-position="top"
alt="Mock Numbers Example" />
{:else}
<img
src={MockNumbersLight}
width="266"
height="171"
class="u-image-object-fit-contain u-only-light u-width-full-line u-height-100-percent"
style:object-position="top"
alt="Mock Numbers Example" />
{/if}
</div>
</svelte:fragment>
<svelte:fragment slot="title">{emptyStateTitle}</svelte:fragment>
{emptyStateDescription}
<svelte:fragment let:source slot="cta">
<Button
class="u-margin-block-start-32"
secondary
fullWidth
href="https://cloud.appwrite.io/register"
external
on:click={() => {
trackEvent('click_cloud_signup', {
from: 'button',
source
});
}}>{cta}</Button>
</svelte:fragment>
</EmptyCardImageCloud>
{:else if numbers?.length > 0}
<ul class="form-list u-gap-8">
{#each numbers as number, index}
<FormItem isMultiple>
<InputPhone
id={`key-${index}`}
bind:value={number.phone}
fullWidth
placeholder="Enter Phone Number"
label="Phone Number"
showLabel={index === 0 ? true : false}
minlength={8}
placeholder="Enter phone number"
label="Phone number"
showLabel={index === 0}
minlength={9}
maxlength={16}
required />
<InputText
required>
<button
slot="options"
use:tooltip={{ content: 'Regenerate', placement: 'bottom' }}
on:click={() => (number.phone = generateNumber())}
class="options-list-button"
aria-label="regenerate text"
type="button">
<span class="icon-refresh" aria-hidden="true"></span>
</button>
</InputPhone>
<InputOTP
id={`value-${index}`}
bind:value={number.otp}
fullWidth
placeholder="Enter value"
label="Verification Code"
showLabel={index === 0 ? true : false}
label="Verification code"
maxlength={6}
required />
<FormItemPart alignEnd>
pattern={'^[0-9]{6}$'}
patternError="The value must contain 6 digits"
showLabel={index === 0}
required>
<button
slot="options"
use:tooltip={{ content: 'Regenerate', placement: 'bottom' }}
on:click={() => (number.otp = generateOTP())}
class="options-list-button"
aria-label="regenerate text"
type="button">
<span class="icon-refresh" aria-hidden="true"></span>
</button>
</InputOTP>
<FormItemPart>
<Button
text
disabled={numbers.length === 0}
class={'u-padding-4 ' +
(index === 0 ? 'u-margin-block-start-24' : '')}
on:click={() => {
deletePhoneNumber(index);
}}>
@@ -101,8 +213,8 @@
text
on:click={() =>
addPhoneNumber({
phone: '',
otp: ''
phone: generateNumber(),
otp: generateOTP()
})}
disabled={numbers.length >= 10}>
<span class="icon-plus" aria-hidden="true" />
@@ -113,15 +225,15 @@
<Empty
on:click={() => {
addPhoneNumber({
phone: '',
otp: ''
phone: generateNumber(),
otp: generateOTP()
});
}}>Create a mock phone number</Empty>
}}>Generate number</Empty>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={submitDisabled} submit>Update</Button>
<Button disabled={isSubmitDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,54 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSwitch } from '$lib/elements/forms';
import { FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
let authSessionAlerts = $project.authSessionAlerts ?? false;
async function updateSessionAlerts() {
try {
await sdk.forConsole.projects.updateSessionAlerts($project.$id, authSessionAlerts);
await invalidate(Dependencies.PROJECT);
addNotification({
type: 'success',
message: 'Updated session alerts.'
});
trackEvent(Submit.AuthSessionAlertsUpdate);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AuthSessionAlertsUpdate);
}
}
</script>
<Form onSubmit={updateSessionAlerts}>
<CardGrid>
<Heading tag="h2" size="7" id="personal-data">Session alerts</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputSwitch
bind:value={authSessionAlerts}
id="authSessionAlerts"
label="Session alerts" />
</FormList>
<p class="text">
Enabling this option will send an email to the users when a new session is created.
</p>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={authSessionAlerts === $project.authSessionAlerts} submit>
Update
</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -44,7 +44,7 @@
<form class="form u-grid u-gap-16">
<ul class="form-list is-multiple">
<InputNumber id="length" label="Length" bind:value={$value} min={0} />
<InputSelect id="period" label="Time Period" bind:value={$unit} {options} />
<InputSelect id="period" label="Time period" bind:value={$unit} {options} />
</ul>
</form>
</svelte:fragment>
@@ -70,7 +70,7 @@
<Pill>recommended</Pill>
</label>
</li>
<li class="form-item is-multiple">
<li class="form-item is-multiple u-cross-center">
<div class="input-text-wrapper">
<label class="choice-item" for="limited">
<input
@@ -80,7 +80,7 @@
type="radio"
bind:group={isLimited}
value={true} />
<div class="choice-item-content">
<div class="choice-item-content u-padding-inline-end-16">
<div class="choice-item-title">Limited</div>
</div>
</label>

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