Merge branch 'main' into 'sync-main-studio'.

This commit is contained in:
Darshan
2025-12-11 15:10:08 +05:30
30 changed files with 469 additions and 290 deletions
@@ -92,8 +92,8 @@
<Modal bind:show={showSelectProject} title="Manage projects" onSubmit={updateSelected}>
<svelte:fragment slot="description">
Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be blocked
after this date.
Choose which {$currentPlan?.projects || 2} projects to keep. Projects over the limit will be
blocked after this date.
</svelte:fragment>
{#if loading}
+49 -37
View File
@@ -2,12 +2,13 @@
import { BillingPlan } from '$lib/constants';
import { formatCurrency } from '$lib/helpers/numbers';
import { currentPlan, organization } from '$lib/stores/organization';
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Badge, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { LabelCard } from '..';
import type { Plan } from '$lib/sdk/billing';
import { page } from '$app/state';
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
import { InputSelect } from '$lib/elements/forms';
import { isFreePlan, isPaidPlan } from '$lib/helpers/billing';
export let disabled = false;
export let isNewOrg = false;
@@ -113,52 +114,63 @@
function handleTierChange(group: PlanGroup) {
billingPlan = selectedTiers[group.key] as BillingPlan;
}
function shouldShowTooltip(plan: Plan) {
if (isPaidPlan(plan.$id)) return true;
else return !anyOrgFree;
}
</script>
<Layout.Stack>
{#each groupedPlans as group (group.key)}
{@const basePlan = group.plans[0]}
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={!selfService || (basePlan.$id === BillingPlan.FREE && anyOrgFree) || disabled}
tooltipShow={basePlan.$id === BillingPlan.FREE && anyOrgFree}
value={group.isGrouped ? selectedTiers[group.key] || basePlan.$id : basePlan.$id}
title={basePlan.name}>
<svelte:fragment slot="action">
{#if group.plans.some((plan) => $organization?.billingPlan === plan.$id) && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Tooltip disabled={shouldShowTooltip(basePlan)} maxWidth="fit-content">
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={!selfService || (isFreePlan(basePlan.$id) && anyOrgFree) || disabled}
tooltipShow={isFreePlan(basePlan.$id) && anyOrgFree}
value={group.isGrouped ? selectedTiers[group.key] || basePlan.$id : basePlan.$id}
title={basePlan.name}>
<svelte:fragment slot="action">
{#if group.plans.some((plan) => $organization?.billingPlan === plan.$id) && !isNewOrg}
<Badge variant="secondary" size="xs" content="Current plan" />
{/if}
</svelte:fragment>
<Layout.Stack direction="column" gap="m">
<Layout.Stack direction="column" gap="xxs">
<Typography.Caption variant="400">
{basePlan.desc}
</Typography.Caption>
<Layout.Stack direction="column" gap="m">
<Layout.Stack direction="column" gap="xxs">
<Typography.Caption variant="400">
{basePlan.desc}
</Typography.Caption>
{#if !group.isGrouped}
<Typography.Text>
{@const isZeroPrice = (basePlan.price ?? 0) <= 0}
{@const price = formatCurrency(basePlan.price ?? 0)}
{#if resolvedProfile.id === ProfileMode.STUDIO}
{getPlanLabel(basePlan)}
{:else}
{isZeroPrice ? price : getPlanLabel(basePlan)}
{/if}
</Typography.Text>
{#if !group.isGrouped}
<Typography.Text>
{@const isZeroPrice = (basePlan.price ?? 0) <= 0}
{@const price = formatCurrency(basePlan.price ?? 0)}
{#if resolvedProfile.id === ProfileMode.STUDIO}
{getPlanLabel(basePlan)}
{:else}
{isZeroPrice ? price : getPlanLabel(basePlan)}
{/if}
</Typography.Text>
{/if}
</Layout.Stack>
{#if group.isGrouped}
<InputSelect
id="tier-{group.key}"
bind:value={selectedTiers[group.key]}
on:change={() => handleTierChange(group)}
options={group.tierOptions} />
{/if}
</Layout.Stack>
</LabelCard>
{#if group.isGrouped}
<InputSelect
id="tier-{group.key}"
bind:value={selectedTiers[group.key]}
on:change={() => handleTierChange(group)}
options={group.tierOptions} />
{/if}
</Layout.Stack>
</LabelCard>
<svelte:fragment slot="tooltip">
Only 1 free organization is allowed per account.
</svelte:fragment>
</Tooltip>
{/each}
{#if $currentPlan && !currentPlanInList}
+11 -3
View File
@@ -33,6 +33,7 @@
import { IconInfo, IconPlus, IconViewGrid, IconViewList } from '@appwrite.io/pink-icons-svelte';
import { showCreateBucket } from '$routes/(console)/project-[region]-[project]/storage/+page.svelte';
import { preferences } from '$lib/stores/preferences';
import { addNotification } from '$lib/stores/notifications';
export let show: boolean;
export let mimeTypeQuery: string = 'image/';
@@ -110,7 +111,10 @@
}
selectFile(file);
} catch (e) {
console.error(e);
addNotification({
type: 'error',
message: e.message
});
} finally {
uploading = false;
}
@@ -351,7 +355,9 @@
<Layout.Stack gap="l">
<Typography.Title size="s" color="--fgcolor-neutral-primary"
>{localFileBucketTitle}</Typography.Title>
<Upload.Dropzone bind:files={localFile} extensions={[allowedExtension]}>
<Upload.Dropzone
bind:files={localFile}
extensions={allowedExtension === '*' ? undefined : [allowedExtension]}>
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack alignItems="center" gap="s">
<Layout.Stack
@@ -370,7 +376,9 @@
<Icon icon={IconInfo} size="s" />
</Layout.Stack>
<svelte:fragment slot="tooltip"
>{allowedExtension} files are allowed</svelte:fragment>
>{allowedExtension === '*'
? `${mimeTypeQuery} files are allowed`
: `${allowedExtension} files are allowed`}</svelte:fragment>
</Tooltip>
</Layout.Stack>
<Typography.Caption variant="400"
+7 -2
View File
@@ -14,6 +14,7 @@
import type { Column } from '$lib/helpers/types';
import type { Writable } from 'svelte/store';
import { TagList } from '.';
import { toLocalDateTimeISO } from '$lib/helpers/date';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
@@ -79,7 +80,7 @@
value = column?.array ? [] : null;
if (column?.type === 'datetime') {
const now = new Date();
value = now.toISOString().slice(0, 16);
value = toLocalDateTimeISO(now.toISOString()).slice(0, 16);
}
// Initialize spatial data with default values
if (column?.type === 'point') {
@@ -108,7 +109,11 @@
if (isDistanceOperator && distanceValue !== null && value !== null) {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues, distanceValue);
} else {
addFilter(columnsArray, columnId, operatorKey, value, arrayValues);
const preparedValue =
column?.type === 'datetime' && typeof value === 'string' && value
? new Date(value).toISOString()
: value;
addFilter(columnsArray, columnId, operatorKey, preparedValue, arrayValues);
}
columnId = null;
@@ -57,7 +57,11 @@
}
function addCondition() {
const newTag = generateTag(selectedColumn, operatorKey, value || arrayValues);
const preparedValue =
column?.type === 'datetime' && typeof value === 'string' && value
? new Date(value).toISOString()
: value;
const newTag = generateTag(selectedColumn, operatorKey, preparedValue || arrayValues);
if (localTags.some((t) => t.tag === newTag.tag && t.value === newTag.value)) {
return;
} else {
@@ -66,7 +70,7 @@
{
id: selectedColumn,
operator: operatorKey,
value: value,
value: preparedValue,
arrayValues: arrayValues
}
];
+126 -109
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { EmptySearch, Paginator } from '$lib/components';
import { EmptySearch, PaginationInline } from '$lib/components';
import { Button, InputSearch, InputSelect } from '$lib/elements/forms';
import { timeFromNow } from '$lib/helpers/date';
import { sdk } from '$lib/stores/sdk';
@@ -17,7 +17,7 @@
import { IconLockClosed, IconPlus } from '@appwrite.io/pink-icons-svelte';
import ConnectGit from './connectGit.svelte';
import SvgIcon from '../svgIcon.svelte';
import { VCSDetectionType, type Models } from '@appwrite.io/console';
import { Query, VCSDetectionType, type Models } from '@appwrite.io/console';
import { getFrameworkIcon } from '$lib/stores/sites';
import { connectGitHub } from '$lib/stores/git';
import { page } from '$app/state';
@@ -48,6 +48,8 @@
let selectedInstallation = $state(null);
let isLoadingRepositories = $state(null);
let installationsMap = $state(null);
let offset = $state(0);
const limit = 5;
onMount(() => {
loadInstallations();
@@ -65,8 +67,18 @@
300
);
const loadRepositoryPage = async () => {
isLoadingRepositories = true;
try {
await loadRepositories(selectedInstallation, search);
} finally {
isLoadingRepositories = false;
}
};
$effect(() => {
if (selectedInstallation && search !== undefined) {
offset = 0; // reset offset to 0 when search changes
debouncedLoadRepositories(selectedInstallation, search);
}
});
@@ -103,27 +115,23 @@
}
async function loadRepositories(installationId: string, search: string) {
if (product === 'functions') {
$repositories.repositories = (
(await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositories({
installationId,
type: VCSDetectionType.Runtime,
search: search || undefined
})) as unknown as Models.ProviderRepositoryRuntimeList
).runtimeProviderRepositories; //TODO: remove forced cast after backend fixes
} else {
$repositories.repositories = (
(await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositories({
installationId,
type: VCSDetectionType.Framework,
search: search || undefined
})) as unknown as Models.ProviderRepositoryFrameworkList
).frameworkProviderRepositories;
}
const result = await sdk
.forProject(page.params.region, page.params.project)
.vcs.listRepositories({
installationId,
type:
product === 'functions' ? VCSDetectionType.Runtime : VCSDetectionType.Framework,
search: search || undefined,
queries: [Query.limit(limit), Query.offset(offset)]
});
$repositories.repositories =
product === 'functions'
? (result as unknown as Models.ProviderRepositoryRuntimeList)
.runtimeProviderRepositories
: (result as unknown as Models.ProviderRepositoryFrameworkList)
.frameworkProviderRepositories; //TODO: remove forced cast after backend fixes
$repositories.total = result.total;
$repositories.search = search;
$repositories.installationId = installationId;
return $repositories.repositories;
@@ -188,94 +196,85 @@
{#if selectedInstallation}
<!-- manual installation change -->
{#if isLoadingRepositories}
<SkeletonRepoList />
{:else if $repositories?.repositories?.length}
<Paginator
items={$repositories.repositories}
hideFooter={$repositories.repositories?.length <= 6}
limit={6}>
{#snippet children(
paginatedItems: Models.ProviderRepositoryRuntime[] &
Models.ProviderRepositoryFramework[]
)}
<Table.Root columns={1} let:root>
{#each paginatedItems as repo}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
onchange={() => repository.set(repo)}
value={repo.id} />
<SkeletonRepoList count={limit} />
{:else if $repositories.total > 0}
<Table.Root columns={1} let:root>
{#each $repositories.repositories as repo}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if action === 'select'}
<input
class="is-small u-margin-inline-end-8"
type="radio"
name="repositories"
bind:group={selectedRepository}
onchange={() => repository.set(repo)}
value={repo.id} />
{/if}
{#if product === 'sites'}
{#if 'framework' in repo && repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<SvgIcon
name={getFrameworkIcon(repo.framework)}
iconSize="small" />
</Avatar>
{:else}
<Avatar size="xs" alt={repo.name} empty />
{/if}
{:else}
{@const iconName =
'runtime' in repo && repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar size="xs" alt={repo.name} empty={!iconName}>
{#if iconName}
<SvgIcon name={iconName} iconSize="small" />
{/if}
{#if product === 'sites'}
{#if repo?.framework && repo.framework !== 'other'}
<Avatar size="xs" alt={repo.name}>
<SvgIcon
name={getFrameworkIcon(repo.framework)}
iconSize="small" />
</Avatar>
{:else}
<Avatar size="xs" alt={repo.name} empty />
{/if}
{:else}
{@const iconName = repo?.runtime
? repo.runtime.split('-')[0]
: undefined}
<Avatar size="xs" alt={repo.name} empty={!iconName}>
{#if iconName}
<SvgIcon name={iconName} iconSize="small" />
{/if}
</Avatar>
{/if}
<Layout.Stack
direction="row"
alignItems="center"
gap="s"
style="flex: 1; min-width: 0;">
<Typography.Text
</Avatar>
{/if}
<Layout.Stack
direction="row"
alignItems="center"
gap="s"
style="flex: 1; min-width: 0;">
<Typography.Text
truncate
color="--fgcolor-neutral-secondary"
style="flex: 1; min-width: 0;">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
{#if !$isSmallViewport}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-secondary"
style="flex: 1; min-width: 0;">
{repo.name}
</Typography.Text>
{#if repo.private}
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
{/if}
{#if !$isSmallViewport}
<time datetime={repo.pushedAt}>
<Typography.Caption
variant="400"
truncate
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
{/if}
</Layout.Stack>
{#if action === 'button'}
<PinkButton.Button
size="xs"
variant="secondary"
style="flex-shrink: 0;"
on:click={() => connect(repo)}>
Connect
</PinkButton.Button>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{/snippet}
</Paginator>
color="--fgcolor-neutral-tertiary">
{timeFromNow(repo.pushedAt)}
</Typography.Caption>
</time>
{/if}
</Layout.Stack>
{#if action === 'button'}
<PinkButton.Button
size="xs"
variant="secondary"
style="flex-shrink: 0;"
on:click={() => connect(repo)}>
Connect
</PinkButton.Button>
{/if}
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{:else if search}
<EmptySearch hidePages hidePagination bind:search target="repositories">
<svelte:fragment slot="actions">
@@ -293,6 +292,24 @@
</Layout.Stack>
</Card>
{/if}
{#if isLoadingRepositories || $repositories.total > 0}
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center"
wrap="wrap">
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
Total results: {$repositories.total}
</Typography.Text>
<PaginationInline
{limit}
bind:offset
total={$repositories.total}
hidePages={true}
on:change={loadRepositoryPage} />
</Layout.Stack>
{/if}
{/if}
</Layout.Stack>
{:else}
@@ -1,9 +1,11 @@
<script lang="ts">
import { Layout, Table, Skeleton } from '@appwrite.io/pink-svelte';
const { count = 4 }: { count?: number } = $props();
</script>
<Table.Root columns={1} let:root>
{#each Array(4) as _}
{#each Array(count) as _}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center">
+9 -3
View File
@@ -221,20 +221,26 @@
<Typography.Text
size="m"
color="--fgcolor-neutral-primary">
{data.customName}
{formatName(
data.customName ?? '',
$isSmallViewport ? 20 : 28
)}
</Typography.Text>
</Layout.Stack>
{#if data.roleName}
<InteractiveText
isVisible
variant="copy"
text={data.roleName}
text={formatName(
data.roleName,
$isSmallViewport ? 20 : 28
)}
value={data.roleName} />
{:else}
<InteractiveText
isVisible
variant="copy"
text={role}
text={formatName(role, $isSmallViewport ? 20 : 28)}
value={role} />
{/if}
</Layout.Stack>
+10 -1
View File
@@ -12,6 +12,8 @@
export let optionalText: string = null;
export let tooltip: string = null;
export let isPopoverDefined = true;
export let mimeTypeQuery: string = 'image/';
export let allowedExtension: string = '*';
let show = false;
@@ -93,5 +95,12 @@
</div>
{#if show}
<FilePicker selectedFile={value?.$id} selectedBucket={value?.bucketId} bind:show {onSelect} />
<FilePicker
selectedFile={value?.$id}
selectedBucket={value?.bucketId}
bind:show
{onSelect}
showLocalFileBucket
{mimeTypeQuery}
{allowedExtension} />
{/if}
+2 -2
View File
@@ -1,8 +1,8 @@
import { page } from '$app/stores';
import { derived, writable } from 'svelte/store';
import type { Models, Platform } from '@appwrite.io/console';
import type { Tier } from './billing';
import type { Plan } from '$lib/sdk/billing';
import { derived, writable } from 'svelte/store';
import { type Models, Platform } from '@appwrite.io/console';
export type OrganizationError = {
status: number;
@@ -58,6 +58,7 @@
let newProjName = '';
let projectType: 'existing' | 'new' = 'existing';
let newlyCreatedProject: Models.Project | null = null;
async function getProjects(orgId: string | null) {
if (!orgId) {
@@ -128,8 +129,9 @@
});
onExit();
await invalidate(Dependencies.PROJECTS);
const targetProject = newlyCreatedProject ?? currentSelectedProject;
await goto(
`${base}/project-${currentSelectedProject.region}-${currentSelectedProject.$id}/settings/migrations`
`${base}/project-${targetProject.region ?? 'default'}-${targetProject.$id}/settings/migrations`
);
} catch (error) {
addNotification({
@@ -260,6 +262,7 @@
} else {
const project = await createNewProject();
if (project !== null) {
newlyCreatedProject = project;
projectSdkInstance = sdk.forProject(
project.region,
project.$id
@@ -1,6 +1,6 @@
<script lang="ts">
import { afterNavigate, goto, invalidate, preloadData } from '$app/navigation';
import { base } from '$app/paths';
import { base, resolve } from '$app/paths';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { PlanComparisonBox, PlanSelection, SelectPaymentMethod } from '$lib/components/billing';
@@ -20,33 +20,35 @@
import { writable } from 'svelte/store';
import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte';
import { onMount } from 'svelte';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import type { PageProps } from './$types';
import { isFreePlan } from '$lib/helpers/billing.js';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export let data;
const { data }: PageProps = $props();
let selectedPlan: BillingPlan = data.plan as BillingPlan;
let selectedCoupon: Partial<Coupon> | null = data.coupon;
let previousPage: string = base;
let showExitModal = false;
let showExitModal = $state(false);
let previousPage: string = $state(resolve('/(console)'));
let selectedPlan: BillingPlan = $state(data.plan as BillingPlan);
let selectedCoupon: Partial<Coupon> | null = $state(data.coupon);
let formComponent: Form;
let isSubmitting = writable(false);
let isSubmitting = $state(writable(false));
let formComponent: Form | null = $state(null);
let name: string;
let billingPlan: BillingPlan = BillingPlan.FREE;
let paymentMethodId: string;
let collaborators: string[] = [];
let taxId: string;
let name: string | null = $state(null);
let taxId: string | null = $state(null);
let collaborators: string[] = $state([]);
let paymentMethodId: string | null = $state(null);
let billingPlan: BillingPlan = $state(BillingPlan.FREE);
let billingBudget: number;
let showCreditModal = false;
let showCreditModal = $state(false);
let billingBudget: number | undefined = $state(undefined);
// check if the plan allows multiple members
$: planDetails = $plansInfo?.get(selectedPlan);
$: seatsAddon = planDetails?.addons?.seats;
$: canAddMembers =
!seatsAddon || (seatsAddon.supported ?? false) || (seatsAddon.limit ?? 0) > 1;
const planDetails = $derived($plansInfo?.get(selectedPlan));
const seatsAddon = $derived(planDetails?.addons?.seats);
const canAddMembers = $derived(
!seatsAddon || (seatsAddon.supported ?? false) || (seatsAddon.limit ?? 0) > 1
);
$: if (!canAddMembers && collaborators.length > 0) {
collaborators = [];
@@ -60,8 +62,7 @@
if (page.url.searchParams.has('coupon')) {
const coupon = page.url.searchParams.get('coupon');
try {
const response = await sdk.forConsole.billing.getCouponAccount(coupon);
selectedCoupon = response;
selectedCoupon = await sdk.forConsole.billing.getCouponAccount(coupon);
} catch (e) {
selectedCoupon = {
code: null,
@@ -218,7 +219,8 @@
<PlanSelection
isNewOrg
disabled={$isSubmitting}
bind:billingPlan={selectedPlan} />
bind:billingPlan={selectedPlan}
anyOrgFree={data.hasFreeOrganizations} />
</Layout.Stack>
</Fieldset>
@@ -55,7 +55,7 @@
async function handleSubmit() {
isButtonDisabled = true;
try {
if (paymentMethodId === null) {
if (paymentMethodId === null || paymentMethodId === '$new') {
try {
if (showState && !state) {
throw Error('Please select a state');
@@ -65,13 +65,17 @@
method = await setPaymentMethod(paymentMethod.id, name, state);
} else {
const card = await submitStripeCard(name, $organization.$id);
if (card && Object.hasOwn(card, 'id')) {
// When Stripe returns an expanded PaymentMethod for US cards, we need state.
if (Object.hasOwn(card, 'id') && (card as PaymentMethod)?.card) {
if ((card as PaymentMethod).card?.country === 'US') {
paymentMethod = card as PaymentMethod;
showState = true;
return;
}
} else if (card && Object.hasOwn(card, '$id')) {
}
// Otherwise, we expect an Appwrite PaymentMethodData with `$id`.
if (Object.hasOwn(card, '$id')) {
method = card as PaymentMethodData;
}
}
@@ -344,6 +344,7 @@
<PlanSelection
disabled={$isSubmitting}
anyOrgFree={data.hasFreeOrgs}
selfService={data.selfService}
bind:billingPlan={selectedPlan} />
@@ -382,7 +383,8 @@
{:else}
After switching plans,
<b
>you will be charged {price} monthly for {extraMembers} team members.</b>
>you will be charged {price} monthly for {extraMembers} team
members.</b>
This will be reflected in your next invoice.
{/if}
</Alert.Inline>
@@ -1,4 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { AvatarGroup, Tab, Tabs } from '$lib/components';
@@ -14,7 +15,12 @@
readOnly,
tierToPlan
} from '$lib/stores/billing';
import { members, newMemberModal, type Organization } from '$lib/stores/organization';
import {
members,
newMemberModal,
newOrgModal,
type Organization
} from '$lib/stores/organization';
import {
canSeeBilling,
canSeeProjects,
@@ -23,7 +29,7 @@
isOwner
} from '$lib/stores/roles';
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { IconGithub, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { IconGithub, IconPlus, IconPlusSm } from '@appwrite.io/pink-icons-svelte';
import { Badge, Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import { isFreePlan } from '$lib/helpers/billing';
@@ -107,7 +113,7 @@
{#if shouldShowCover}
<Cover blocksize="152px">
<svelte:fragment slot="header">
<span class="u-flex u-cross-center u-gap-8 u-min-width-0">
<Layout.Stack direction="row" alignItems="center" gap="m" class="u-min-width-0">
<Typography.Title color="--fgcolor-neutral-primary" size="xl" truncate>
{titleName}
</Typography.Title>
@@ -132,8 +138,15 @@
</Tooltip>
{/if}
{/if}
</span>
<Button
secondary
icon
size="xs"
on:click={() =>
isCloud ? goto(`${base}/create-organization`) : newOrgModal.set(true)}>
<Icon icon={IconPlusSm} size="m" />
</Button>
</Layout.Stack>
<div class="u-margin-inline-start-auto">
<Layout.Stack direction="row" alignItems="center" gap="xl">
{#if $members.total > 1}
@@ -1,10 +1,9 @@
import { Dependencies } from '$lib/constants';
import { defaultRoles, defaultScopes, Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import type { LayoutLoad } from './$types';
import { preferences } from '$lib/stores/preferences';
import { failedInvoice } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { defaultRoles, defaultScopes } from '$lib/constants';
import { get } from 'svelte/store';
import { headerAlert } from '$lib/stores/headerAlert';
import PaymentFailed from '$lib/components/billing/alerts/paymentFailed.svelte';
@@ -0,0 +1,65 @@
import type { Columns } from '../table-[table]/store';
import type { RealtimeResponse } from '$lib/stores/sdk';
export function setupColumnObserver() {
let expectedCount = 0;
let resolvePromise: () => void;
let timeout: ReturnType<typeof setTimeout>;
let isActive = true;
const availableColumns = new Set<string>();
const waitPromise = new Promise<void>((resolve) => (resolvePromise = resolve));
const columnCreationHandler = (response: RealtimeResponse) => {
if (!isActive) return;
const { events, payload } = response;
if (
events.includes('databases.*.tables.*.columns.*.create') ||
events.includes('databases.*.tables.*.columns.*.update')
) {
const asColumn = payload as Columns;
const columnId = asColumn.key;
const status = asColumn.status;
if (status === 'available') {
availableColumns.add(columnId);
if (expectedCount > 0 && availableColumns.size >= expectedCount) {
clearTimeout(timeout);
cleanup();
resolvePromise();
}
}
}
};
const cleanup = () => {
isActive = false;
if (timeout) clearTimeout(timeout);
};
// return function to start waiting!
const startWaiting = (count: number) => {
expectedCount = count;
timeout = setTimeout(() => {
cleanup();
resolvePromise();
}, 10000);
if (availableColumns.size >= expectedCount) {
clearTimeout(timeout);
cleanup();
resolvePromise();
}
};
return {
cleanup,
waitPromise,
startWaiting,
columnCreationHandler
};
}
@@ -19,8 +19,9 @@
import { preferences } from '$lib/stores/preferences';
import SpreadsheetContainer from '../table-[table]/layout/spreadsheet.svelte';
import { onDestroy, onMount, tick } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { sdk, realtime, type RealtimeResponse } from '$lib/stores/sdk';
import { page } from '$app/state';
import { setupColumnObserver } from '../(observer)/columnObserver';
import {
type ColumnInput,
mapSuggestedColumns,
@@ -125,6 +126,7 @@
// let tooltipTopPosition = $state(50);
let triggerColumnId = $state<string | null>(null);
let hoveredColumnId = $state<string | null>(null);
let columnCreationHandler: ((response: RealtimeResponse) => void) | null = null;
// for deleting a column + undo
let undoTimer: ReturnType<typeof setTimeout> | null = $state(null);
@@ -585,7 +587,7 @@
($isSmallViewport ? 14 : 17) + (!$expandTabs ? 2 : 0) - userDataRows.length
);
onMount(async () => {
onMount(() => {
columnsOrder.set(preferences.getColumnOrder(tableId));
columnsWidth.set(preferences.getColumnWidths(tableId));
@@ -595,7 +597,13 @@
}
requestAnimationFrame(recalcAll);
await suggestColumns();
suggestColumns();
return realtime.forProject(page.params.region, ['project', 'console'], (response) => {
if (response.events.includes('databases.*.tables.*.columns.*')) {
columnCreationHandler?.(response);
}
});
});
function resetSuggestionsStore(fullReset: boolean = true) {
@@ -968,6 +976,14 @@
}
try {
const {
startWaiting,
waitPromise,
columnCreationHandler: handler
} = setupColumnObserver();
columnCreationHandler = handler;
const results = [];
for (const column of customColumns) {
@@ -1066,6 +1082,9 @@
results.push(columnResult);
}
startWaiting(customColumns.length);
await waitPromise;
await invalidate(Dependencies.TABLE);
addNotification({
@@ -1087,6 +1106,8 @@
message: error.message
});
creatingColumns = false;
} finally {
columnCreationHandler = null;
}
}
@@ -390,6 +390,8 @@
left: 1.25rem;
position: absolute;
padding-block-end: 1rem;
z-index: 1;
background: var(--bgcolor-neutral-primary);
}
.action-menu-divider {
@@ -74,15 +74,20 @@
import { getPageTitle } from '../../../store';
import { isTabletViewport } from '$lib/stores/viewport';
import { showColumnsSuggestionsModal } from '../(suggestions)';
import IndexesSuggestions from '../(suggestions)/indexes.svelte';
import ColumnsSuggestions from '../(suggestions)/columns.svelte';
import { showColumnsSuggestionsModal } from '../(suggestions)';
import { setupColumnObserver } from '../(observer)/columnObserver';
import { resolvedProfile } from '$lib/profiles/index.svelte';
let editRow: EditRow;
let editRelatedRow: EditRelatedRow;
let editRowPermissions: EditRowPermissions;
let editRowDisabled = true;
let editRelatedRowDisabled = true;
let editRowPermissionsDisabled = true;
let createIndex: CreateIndex;
let createColumn: CreateColumn;
let selectedOption: Option['name'] = 'String';
@@ -278,56 +283,6 @@
indexes: 700
});
function setupColumnObserver() {
let expectedCount = 0;
let resolvePromise: () => void;
let timeout: ReturnType<typeof setTimeout>;
const availableColumns = new Set<string>();
const waitPromise = new Promise<void>((resolve) => (resolvePromise = resolve));
columnCreationHandler = (response: RealtimeResponse) => {
const { events, payload } = response;
if (
events.includes('databases.*.tables.*.columns.*.create') ||
events.includes('databases.*.tables.*.columns.*.update')
) {
const asColumn = payload as Columns;
const columnId = asColumn.key;
const status = asColumn.status;
if (status === 'available') {
availableColumns.add(columnId);
if (expectedCount > 0 && availableColumns.size >= expectedCount) {
clearTimeout(timeout);
columnCreationHandler = null;
resolvePromise();
}
}
}
};
// return function to start waiting!
const startWaiting = (count: number) => {
expectedCount = count;
timeout = setTimeout(() => {
columnCreationHandler = null;
resolvePromise();
}, 10000);
if (availableColumns.size >= expectedCount) {
clearTimeout(timeout);
columnCreationHandler = null;
resolvePromise();
}
};
return { startWaiting, waitPromise };
}
async function createFakeData() {
isWaterfallFromFaker.set(true);
@@ -340,7 +295,14 @@
if (!filteredColumns.length) {
try {
const { startWaiting, waitPromise } = setupColumnObserver();
const {
startWaiting,
waitPromise,
columnCreationHandler: handler
} = setupColumnObserver();
columnCreationHandler = handler;
columns = await generateColumns($project, page.params.database, page.params.table);
startWaiting(columns.length);
await waitPromise;
@@ -356,6 +318,8 @@
});
$spreadsheetLoading = false;
return;
} finally {
columnCreationHandler = null;
}
}
@@ -483,7 +447,7 @@
bind:show={$databaseRowSheetOptions.show}
submit={{
text: 'Update',
disabled: editRow?.isDisabled(),
disabled: editRowDisabled,
onClick: async () => await editRow?.update()
}}
topAction={{
@@ -548,6 +512,7 @@
bind:this={editRow}
bind:row={$databaseRowSheetOptions.row}
bind:rowId={$databaseRowSheetOptions.rowId}
bind:disabled={editRowDisabled}
autoFocus={$databaseRowSheetOptions.autoFocus} />
{/key}
</SideSheet>
@@ -558,13 +523,14 @@
bind:show={$databaseRelatedRowSheetOptions.show}
submit={{
text: 'Update',
disabled: editRelatedRow?.isDisabled(),
disabled: editRelatedRowDisabled,
onClick: async () => await editRelatedRow?.update()
}}>
<EditRelatedRow
bind:this={editRelatedRow}
rows={$databaseRelatedRowSheetOptions.rows}
tableId={$databaseRelatedRowSheetOptions.tableId} />
tableId={$databaseRelatedRowSheetOptions.tableId}
bind:disabledState={editRelatedRowDisabled} />
</SideSheet>
<SideSheet
@@ -589,10 +555,13 @@
bind:show={$rowPermissionSheet.show}
submit={{
text: 'Update',
disabled: editRowPermissions?.disableSubmit(),
disabled: editRowPermissionsDisabled,
onClick: async () => editRowPermissions?.updatePermissions()
}}>
<EditRowPermissions bind:this={editRowPermissions} bind:row={$rowPermissionSheet.row} />
<EditRowPermissions
bind:this={editRowPermissions}
bind:row={$rowPermissionSheet.row}
bind:arePermsDisabled={editRowPermissionsDisabled} />
</SideSheet>
<SideSheet title="Row activity" bind:show={$rowActivitySheet.show} closeOnBlur>
@@ -19,6 +19,7 @@
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
import { deepClone } from '$lib/helpers/object';
import deepEqual from 'deep-equal';
import { onMount } from 'svelte';
const tableId = page.params.table;
const databaseId = page.params.database;
@@ -26,11 +27,13 @@
let {
row = $bindable(),
rowId = $bindable(null),
autoFocus = true
autoFocus = true,
disabled = $bindable(true)
}: {
row?: Models.Row | null;
rowId?: string | null;
autoFocus?: boolean;
disabled?: boolean;
} = $props();
let loading = $state(false);
@@ -38,6 +41,11 @@
let work = $state<Writable<Models.Row> | null>(null);
let columnFormWrapper = $state<HTMLElement | null>(null);
onMount(() => {
/* silences the not read error warning */
disabled;
});
function initWork() {
const filteredKeys = Object.keys(row).filter((key) => {
return !PROHIBITED_ROW_KEYS.includes(key);
@@ -157,11 +165,14 @@
}
}
export function isDisabled(): boolean {
if (!work || !row || !$table?.columns?.length) return true;
$effect(() => {
if (!work || !row || !$table?.columns?.length) {
disabled = true;
return;
}
return $table.columns.every((column) => compareColumns(column, $work, row));
}
disabled = $table.columns.every((column) => compareColumns(column, $work, row));
});
function focusFirstInput() {
const firstInput = columnFormWrapper?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
@@ -10,20 +10,23 @@
import { addNotification } from '$lib/stores/notifications';
import { symmetricDifference } from '$lib/helpers/array';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { onMount } from 'svelte';
let {
row = $bindable(null)
row = $bindable(null),
arePermsDisabled = $bindable(true)
}: {
row: Models.DefaultRow | Models.Row;
arePermsDisabled?: boolean;
} = $props();
let permissions = $state(row.$permissions);
let arePermsDisabled = $state(true);
let showPermissionAlert = $state(true);
let permissions = $state(row.$permissions);
export function disableSubmit() {
return arePermsDisabled;
}
onMount(() => {
/* silences the not read error warning */
arePermsDisabled;
});
export async function updatePermissions() {
try {
@@ -14,26 +14,32 @@
import { Accordion, Layout, Skeleton } from '@appwrite.io/pink-svelte';
import { deepClone } from '$lib/helpers/object';
import { preferences } from '$lib/stores/preferences';
import { onMount } from 'svelte';
const databaseId = page.params.database;
let {
rows,
tableId
tableId,
disabledState = $bindable(true)
}: {
rows: string | Models.Row[];
tableId: string;
disabledState?: boolean;
} = $props();
let loading = $state(false);
let fetchedRows = $state<Models.Row[]>([]);
let relatedTable = $state<Models.Table | null>(null);
let disabledState = $state(calculateAndCompareDisabledState());
let workData = $state<Map<string, Writable<Models.Row>>>(new Map());
let columnFormWrapper = $state<HTMLElement | null>(null);
onMount(() => {
/* silences the not read error warning */
disabledState;
});
function isSingleStore() {
return typeof rows === 'string';
}
@@ -287,10 +293,6 @@
}
}
export function isDisabled(): boolean {
return disabledState;
}
function focusFirstInput() {
const firstInput = columnFormWrapper?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
'input:not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly])'
@@ -335,6 +337,10 @@
});
}
});
$effect(() => {
disabledState = calculateAndCompareDisabledState();
});
</script>
{#if loading}
@@ -901,9 +901,9 @@
hoverEffect
showSelectOnHover
valueWithoutHover={row.$sequence}>
{#each $tableColumns as { id: columnId, isEditable } (columnId)}
{#each $tableColumns as { id: columnId, isEditable, hide } (columnId)}
{@const rowColumn = $columns.find((col) => col.key === columnId)}
{#if columnId === '$id'}
{#if columnId === '$id' && !hide}
<button
on:mouseenter={() => {
showExpandIconForId = index;
@@ -17,10 +17,15 @@ export const proxyRuleList = derived(
export const repositories: Writable<{
search: string;
installationId: string;
repositories: Models.ProviderRepository[];
total: number;
repositories:
| Models.ProviderRepository[]
| Models.ProviderRepositoryFramework[]
| Models.ProviderRepositoryRuntime[];
}> = writable({
search: '',
installationId: '',
total: 0,
repositories: []
});
@@ -19,11 +19,11 @@ import {
} from './components';
export const columns = writable<Column[]>([
{ id: '$id', title: 'Provider ID', type: 'string' },
{ id: 'name', title: 'Name', type: 'string' },
{ id: 'provider', title: 'Provider', type: 'string' },
{ id: 'type', title: 'Type', type: 'string' },
{ id: 'enabled', title: 'Status', type: 'boolean' }
{ id: '$id', title: 'Provider ID', type: 'string', width: 200 },
{ id: 'name', title: 'Name', type: 'string', width: { min: 120 } },
{ id: 'provider', title: 'Provider', type: 'string', width: { min: 120 } },
{ id: 'type', title: 'Type', type: 'string', width: { min: 120 } },
{ id: 'enabled', title: 'Status', type: 'boolean', width: { min: 120 } }
]);
export type ProviderInput = {
@@ -1,4 +1,5 @@
<script lang="ts" context="module">
import { columns } from './store';
import { wizard } from '$lib/stores/wizard';
import CreateAndroid from './createAndroid.svelte';
import CreateApple from './createApple.svelte';
@@ -121,12 +122,13 @@
</script>
{#if data.platforms.platforms.length}
<Table.Root columns={4} let:root>
<Table.Root columns={$columns} let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Name</Table.Header.Cell>
<Table.Header.Cell {root}>Platform type</Table.Header.Cell>
<Table.Header.Cell {root}>Identifier</Table.Header.Cell>
<Table.Header.Cell {root}>Last updated</Table.Header.Cell>
{#each $columns as column}
<Table.Header.Cell {root} column={column.id}>
{column.title}
</Table.Header.Cell>
{/each}
</svelte:fragment>
{#each data.platforms.platforms as platform}
<Table.Row.Link href={`${path}/${platform.$id}`} {root}>
@@ -1,5 +1,14 @@
import type { ComponentType } from 'svelte';
import { PlatformType } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import type { Column } from '$lib/helpers/types';
export const columns = writable<Column[]>([
{ id: 'name', title: 'Name', type: 'string', width: { min: 120 } },
{ id: 'type', title: 'Platform type', type: 'string', width: { min: 120 } },
{ id: 'identifier', title: 'Identifier', type: 'string', width: { min: 120 } },
{ id: '$updatedAt', title: 'Last updated', type: 'string', width: { min: 120 } }
]);
export type PlatformProps = {
key?: string;
@@ -53,7 +53,7 @@
{/if}
<PaginationWithLimit
name="Databases"
name="Buckets"
limit={data.limit}
offset={data.offset}
total={data.buckets.total} />
@@ -418,7 +418,7 @@
placeholder="Select or type user labels"
bind:tags={extensions} />
{/key}
<Layout.Stack direction="row">
<Layout.Stack direction="row" wrap="wrap">
{#each suggestedExtensions as ext}
<Tag
size="s"