mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into 'sync-main-studio'.
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
+65
@@ -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
|
||||
};
|
||||
}
|
||||
+24
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -390,6 +390,8 @@
|
||||
left: 1.25rem;
|
||||
position: absolute;
|
||||
padding-block-end: 1rem;
|
||||
z-index: 1;
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
}
|
||||
|
||||
.action-menu-divider {
|
||||
|
||||
+26
-57
@@ -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>
|
||||
|
||||
+16
-5
@@ -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>(
|
||||
|
||||
+9
-6
@@ -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 {
|
||||
|
||||
+13
-7
@@ -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}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+6
-1
@@ -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} />
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user