mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Merge branch 'main' into fix-SER-467-git-details-not-showing
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
name: Dockerize Profiles
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [feat-profiles]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: [feat-profiles]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dockerize-profiles:
|
||||
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: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: appwrite/console-profiles
|
||||
tags: |
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=gh-${{ github.run_id}}
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- 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 }}
|
||||
@@ -12,16 +12,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: E2E Tests
|
||||
run: pnpm run e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -15,21 +15,29 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Audit dependencies
|
||||
run: pnpm audit --audit-level high
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Svelte Diagnostics
|
||||
run: pnpm run check
|
||||
|
||||
- name: Linter
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Unit Tests
|
||||
run: pnpm run test
|
||||
|
||||
- name: Build Console
|
||||
run: pnpm run build
|
||||
|
||||
@@ -101,4 +101,23 @@ src/
|
||||
5. Before commit: `pnpm run check && pnpm run format && pnpm run lint && pnpm run test && pnpm run build`
|
||||
6. **Take screenshots**: For any UI changes, capture screenshots and include them in the PR description or comments before finalizing
|
||||
|
||||
## Required Pre-Completion Checklist
|
||||
|
||||
**CRITICAL**: Before finishing any work or marking a task complete, agents MUST run the following commands in order and ensure all pass:
|
||||
|
||||
1. **`pnpm run format`** - Auto-fix all formatting issues
|
||||
2. **`pnpm run check`** - Verify TypeScript/Svelte types (must show 0 errors, 0 warnings)
|
||||
3. **`pnpm run lint`** - Check code style (ignore pre-existing issues in files you didn't modify)
|
||||
4. **`pnpm run test`** - Run all unit tests (all tests must pass)
|
||||
5. **`pnpm run build`** - Ensure production build succeeds
|
||||
|
||||
If any command fails:
|
||||
|
||||
- **Format/Lint**: Run `pnpm run format` to auto-fix, then re-check
|
||||
- **Type errors**: Fix all TypeScript errors in files you modified
|
||||
- **Test failures**: Fix failing tests or ensure failures are unrelated to your changes
|
||||
- **Build failures**: Debug and resolve build issues before proceeding
|
||||
|
||||
**Never skip these checks** - they are mandatory quality gates before any work is considered complete.
|
||||
|
||||
**Trust these instructions** - only search if incomplete/incorrect. See CONTRIBUTING.md for PR conventions. Use `--frozen-lockfile` always. Docker builds: multi-stage, final image is nginx serving static files from `/console` path.
|
||||
|
||||
+4
-4
@@ -22,11 +22,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@acef319",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@6916470",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
@@ -95,5 +95,5 @@
|
||||
"svelte-preprocess"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.20.0"
|
||||
"packageManager": "pnpm@10.18.3"
|
||||
}
|
||||
|
||||
Generated
+15
-15
@@ -12,20 +12,20 @@ importers:
|
||||
specifier: ^1.1.24
|
||||
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
|
||||
'@appwrite.io/console':
|
||||
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752
|
||||
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@acef319
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@acef319
|
||||
'@appwrite.io/pink-icons':
|
||||
specifier: 0.25.0
|
||||
version: 0.25.0
|
||||
'@appwrite.io/pink-icons-svelte':
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521(svelte@5.25.3)
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@6916470
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@6916470(svelte@5.25.3)
|
||||
'@appwrite.io/pink-legacy':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@appwrite.io/pink-svelte':
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521(svelte@5.25.3)
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb(svelte@5.25.3)
|
||||
'@faker-js/faker':
|
||||
specifier: ^9.9.0
|
||||
version: 9.9.0
|
||||
@@ -260,8 +260,8 @@ packages:
|
||||
'@analytics/type-utils@0.6.2':
|
||||
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
|
||||
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752':
|
||||
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752}
|
||||
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@acef319':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@acef319}
|
||||
version: 1.10.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
|
||||
@@ -269,8 +269,8 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521}
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@6916470':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@6916470}
|
||||
version: 2.0.0-RC.1
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -284,8 +284,8 @@ packages:
|
||||
'@appwrite.io/pink-legacy@1.0.3':
|
||||
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521}
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb}
|
||||
version: 2.0.0-RC.2
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -3703,13 +3703,13 @@ snapshots:
|
||||
|
||||
'@analytics/type-utils@0.6.2': {}
|
||||
|
||||
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752': {}
|
||||
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@acef319': {}
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@6916470(svelte@5.25.3)':
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
@@ -3722,7 +3722,7 @@ snapshots:
|
||||
'@appwrite.io/pink-icons': 1.0.0
|
||||
the-new-css-reset: 1.11.3
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@4472521(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb(svelte@5.25.3)':
|
||||
dependencies:
|
||||
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
|
||||
'@floating-ui/dom': 1.6.13
|
||||
|
||||
@@ -153,6 +153,7 @@ export enum Click {
|
||||
DatabaseTableDelete = 'click_table_delete',
|
||||
DatabaseDatabaseDelete = 'click_database_delete',
|
||||
DatabaseImportCsv = 'click_database_import_csv',
|
||||
DatabaseExportCsv = 'click_database_export_csv',
|
||||
DomainCreateClick = 'click_domain_create',
|
||||
DomainDeleteClick = 'click_domain_delete',
|
||||
DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification',
|
||||
@@ -195,7 +196,10 @@ export enum Click {
|
||||
VariablesCreateClick = 'click_variable_create',
|
||||
VariablesUpdateClick = 'click_variable_update',
|
||||
VariablesImportClick = 'click_variable_import',
|
||||
WebsiteOpenClick = 'click_open_website'
|
||||
WebsiteOpenClick = 'click_open_website',
|
||||
CopyPromptStarterKitClick = 'click_copy_prompt_starter_kit',
|
||||
OpenInCursorClick = 'click_open_in_cursor',
|
||||
OpenInLovableClick = 'click_open_in_lovable'
|
||||
}
|
||||
|
||||
export enum Submit {
|
||||
@@ -274,6 +278,7 @@ export enum Submit {
|
||||
DatabaseDelete = 'submit_database_delete',
|
||||
DatabaseUpdateName = 'submit_database_update_name',
|
||||
DatabaseImportCsv = 'submit_database_import_csv',
|
||||
DatabaseExportCsv = 'submit_database_export_csv',
|
||||
|
||||
ColumnCreate = 'submit_column_create',
|
||||
ColumnUpdate = 'submit_column_update',
|
||||
@@ -357,6 +362,7 @@ export enum Submit {
|
||||
BucketUpdateSize = 'submit_bucket_update_size',
|
||||
BucketUpdateCompression = 'submit_bucket_update_compression',
|
||||
BucketUpdateExtensions = 'submit_bucket_update_extensions',
|
||||
BucketUpdateTransformations = 'submit_bucket_update_transformations',
|
||||
FileCreate = 'submit_file_create',
|
||||
FileDelete = 'submit_file_delete',
|
||||
FileUpdatePermissions = 'submit_file_update_permissions',
|
||||
|
||||
@@ -30,7 +30,12 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { get, writable, type Readable } from 'svelte/store';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { commandCenterKeyDownHandler, disableCommands, registerCommands } from './commands';
|
||||
import {
|
||||
commandCenterKeyDownHandler,
|
||||
disableCommands,
|
||||
isTargetInputLike,
|
||||
registerCommands
|
||||
} from './commands';
|
||||
import { RootPanel } from './panels';
|
||||
import { addSubPanel, clearSubPanels, subPanels } from './subPanels';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -95,13 +100,9 @@
|
||||
keys = [];
|
||||
}, 1000);
|
||||
|
||||
function isInputEvent(event: KeyboardEvent) {
|
||||
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
|
||||
}
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (!$subPanels.length) {
|
||||
if (isInputEvent(e)) return;
|
||||
if (isTargetInputLike(e.target)) return;
|
||||
keys = [...keys, e.key].slice(-10);
|
||||
resetKeys();
|
||||
}
|
||||
|
||||
@@ -100,8 +100,11 @@ const commandsEnabled = derived(disabledMap, ($disabledMap) => {
|
||||
return Array.from($disabledMap.values()).every((disabled) => !disabled);
|
||||
});
|
||||
|
||||
function isInputEvent(event: KeyboardEvent) {
|
||||
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
|
||||
export function isTargetInputLike(element: EventTarget | null) {
|
||||
if (!(element instanceof HTMLElement)) return false;
|
||||
return !!element.closest(
|
||||
'input,textarea,select,[contenteditable],[role="combobox"],[role="textbox"],[role="searchbox"],[data-command-center-ignore]'
|
||||
);
|
||||
}
|
||||
|
||||
function getCommandRank(command: KeyedCommand) {
|
||||
@@ -204,7 +207,12 @@ export const commandCenterKeyDownHandler = derived(
|
||||
for (const command of commandsArr) {
|
||||
if (!isKeyedCommand(command)) continue;
|
||||
if (!command.forceEnable) {
|
||||
if (command.disabled || !enabled || isInputEvent(event) || $wizard.show) {
|
||||
if (
|
||||
command.disabled ||
|
||||
!enabled ||
|
||||
isTargetInputLike(event.target) ||
|
||||
$wizard.show
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Searcher } from '../commands';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
export const orgSearcher = (async (query: string) => {
|
||||
const { teams } = !isCloud
|
||||
? await sdk.forConsole.teams.list()
|
||||
: await sdk.forConsole.billing.listOrganization();
|
||||
: await sdk.forConsole.billing.listOrganization([Query.equal('platform', 'appwrite')]);
|
||||
|
||||
return teams
|
||||
.filter((organization) => organization.name.toLowerCase().includes(query.toLowerCase()))
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import { DropList, GridItem1, CardContainer, Modal } from '$lib/components';
|
||||
import { GridItem1, CardContainer, Modal } from '$lib/components';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import {
|
||||
Badge,
|
||||
Icon,
|
||||
Typography,
|
||||
Tag,
|
||||
Accordion,
|
||||
ActionMenu,
|
||||
Popover,
|
||||
@@ -20,7 +19,6 @@
|
||||
IconFlutter,
|
||||
IconReact,
|
||||
IconUnity,
|
||||
IconInfo,
|
||||
IconDotsHorizontal,
|
||||
IconInboxIn,
|
||||
IconSwitchHorizontal,
|
||||
@@ -29,7 +27,6 @@
|
||||
import { getPlatformInfo } from '$lib/helpers/platform';
|
||||
import { Status, type Models } from '@appwrite.io/console';
|
||||
import type { ComponentType } from 'svelte';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -52,8 +49,9 @@
|
||||
|
||||
let { projectsToArchive, organization, currentPlan }: Props = $props();
|
||||
|
||||
// Track Read-only info droplist per archived project
|
||||
let readOnlyInfoOpen = $state<Record<string, boolean>>({});
|
||||
// Check if current plan order is less than Pro (order < 1 means FREE plan)
|
||||
let isPlanBelowPro = $derived(currentPlan?.order < 1);
|
||||
|
||||
let showUnarchiveModal = $state(false);
|
||||
let projectToUnarchive = $state<Models.Project | null>(null);
|
||||
let showDeleteModal = $state(false);
|
||||
@@ -97,7 +95,7 @@
|
||||
function isUnarchiveDisabled(): boolean {
|
||||
if (!organization || !currentPlan) return true;
|
||||
|
||||
if (organization.billingPlan === BillingPlan.FREE) {
|
||||
if (isPlanBelowPro) {
|
||||
const currentProjectCount = organization.projects?.length || 0;
|
||||
const projectLimit = currentPlan.projects || 0;
|
||||
|
||||
@@ -196,10 +194,15 @@
|
||||
|
||||
{#if projectsToArchive.length > 0}
|
||||
<div class="archive-projects-margin-top">
|
||||
<Accordion title="Archived projects" badge={`${projectsToArchive.length}`}>
|
||||
<Accordion
|
||||
title={isPlanBelowPro ? 'Archived projects' : 'Pending archive'}
|
||||
badge={`${projectsToArchive.length}`}>
|
||||
<Typography.Text tag="p" size="s">
|
||||
These projects have been archived and are read-only. You can view and migrate their
|
||||
data.
|
||||
{#if isPlanBelowPro}
|
||||
These projects are archived and require a plan upgrade to restore access.
|
||||
{:else}
|
||||
These projects will be archived at the end of your billing cycle.
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
|
||||
<div class="archive-projects-margin">
|
||||
@@ -216,36 +219,6 @@
|
||||
<svelte:fragment slot="title">{formatted}</svelte:fragment>
|
||||
<svelte:fragment slot="status">
|
||||
<div class="status-container">
|
||||
<DropList
|
||||
bind:show={readOnlyInfoOpen[project.$id]}
|
||||
placement="bottom-start"
|
||||
noArrow>
|
||||
<Tag
|
||||
size="s"
|
||||
style="white-space: nowrap;"
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
readOnlyInfoOpen = {
|
||||
...readOnlyInfoOpen,
|
||||
[project.$id]: !readOnlyInfoOpen[project.$id]
|
||||
};
|
||||
}}>
|
||||
<Icon icon={IconInfo} size="s" />
|
||||
<span>Read only</span>
|
||||
</Tag>
|
||||
<svelte:fragment slot="list">
|
||||
<li
|
||||
class="drop-list-item u-width-250"
|
||||
style="padding: var(--space-5, 12px) var(--space-6, 16px)">
|
||||
<span class="u-block u-mb-8">
|
||||
Archived projects are read-only. You can view
|
||||
and migrate their data, but they no longer
|
||||
accept edits or requests.
|
||||
</span>
|
||||
</li>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
<Popover let:toggle padding="none" placement="bottom-end">
|
||||
<Button
|
||||
text
|
||||
@@ -267,6 +240,7 @@
|
||||
>Unarchive project</ActionMenu.Item.Button>
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconSwitchHorizontal}
|
||||
disabled={isUnarchiveDisabled()}
|
||||
on:click={() => handleMigrateProject(project)}
|
||||
>Migrate project</ActionMenu.Item.Button>
|
||||
<div class="action-menu-divider">
|
||||
|
||||
@@ -20,21 +20,23 @@
|
||||
};
|
||||
|
||||
type ButtonProps = {
|
||||
isButton: true;
|
||||
isButton: boolean;
|
||||
href?: never;
|
||||
};
|
||||
|
||||
type AnchorProps = {
|
||||
href: string;
|
||||
isButton?: never;
|
||||
isButton?: boolean;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
let classes = '';
|
||||
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & BaseCardProps;
|
||||
|
||||
export let isDashed = false;
|
||||
export let isButton = false;
|
||||
export let isDashed: boolean = false;
|
||||
export let isButton: boolean = false;
|
||||
export let href: string = null;
|
||||
let classes = '';
|
||||
export let external: boolean = false;
|
||||
export { classes as class };
|
||||
export let style = '';
|
||||
export let padding: $$Props['padding'] = 'm';
|
||||
@@ -45,7 +47,15 @@
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<Card.Link class={resolvedClasses} {href} {style} {padding} {radius} {variant} on:click>
|
||||
<Card.Link
|
||||
{href}
|
||||
{style}
|
||||
{padding}
|
||||
{radius}
|
||||
{variant}
|
||||
on:click
|
||||
class={resolvedClasses}
|
||||
{...external ? { target: '_blank' } : {}}>
|
||||
<Layout.Stack gap="xl">
|
||||
<slot />
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { realtime, sdk } from '$lib/stores/sdk';
|
||||
import { getProjectId } from '$lib/helpers/project';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Layout, Typography, Code } from '@appwrite.io/pink-svelte';
|
||||
import { type Models, type Payload } from '@appwrite.io/console';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
type ExportItem = {
|
||||
status: string;
|
||||
table?: string;
|
||||
bucketId?: string;
|
||||
bucketName?: string;
|
||||
fileName?: string;
|
||||
downloadUrl?: string;
|
||||
errors?: string[];
|
||||
};
|
||||
|
||||
type ExportItemsMap = Map<string, ExportItem>;
|
||||
|
||||
let exportItems = $state<ExportItemsMap>(new Map());
|
||||
|
||||
function downloadExportedFile(downloadUrl: string) {
|
||||
if (!downloadUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(downloadUrl, '_blank');
|
||||
}
|
||||
|
||||
async function showErrorNotification(payload: Payload) {
|
||||
let errorMessage = 'Export failed. Please try again.';
|
||||
try {
|
||||
const parsed = JSON.parse(payload.errors[0]);
|
||||
errorMessage = parsed?.message || errorMessage;
|
||||
} catch {
|
||||
errorMessage = payload.errors[0] || errorMessage;
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
isHtml: true,
|
||||
timeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
async function updateOrAddItem(exportData: Payload | Models.Migration) {
|
||||
if (exportData.destination?.toLowerCase() !== 'csv') return;
|
||||
|
||||
const status = exportData.status;
|
||||
const current = exportItems.get(exportData.$id);
|
||||
let tableName = current?.table;
|
||||
|
||||
// Get bucket, filename, and download URL from migration options
|
||||
const options = ('options' in exportData ? exportData.options : {}) || {};
|
||||
const bucketId = options.bucketId || '';
|
||||
const fileName = options.filename || '';
|
||||
const downloadUrl = options.downloadUrl || '';
|
||||
let bucketName = current?.bucketName;
|
||||
|
||||
const existing = exportItems.get(exportData.$id);
|
||||
|
||||
const isDone = (s: string) => ['completed', 'failed'].includes(s);
|
||||
const isInProgress = (s: string) => ['pending', 'processing'].includes(s);
|
||||
|
||||
// Skip if we're trying to set an in-progress status on a completed migration
|
||||
const shouldSkip = existing && isDone(existing.status) && isInProgress(status);
|
||||
|
||||
const hasNewData =
|
||||
downloadUrl && (!existing?.downloadUrl || existing.downloadUrl !== downloadUrl);
|
||||
const shouldSkipDuplicate = existing?.status === status && !hasNewData;
|
||||
|
||||
if (shouldSkip || shouldSkipDuplicate) return;
|
||||
|
||||
exportItems.set(exportData.$id, {
|
||||
status,
|
||||
table: tableName ?? current?.table,
|
||||
bucketId: bucketId,
|
||||
bucketName: bucketName,
|
||||
fileName: fileName,
|
||||
downloadUrl: downloadUrl,
|
||||
errors: exportData.errors || []
|
||||
});
|
||||
|
||||
exportItems = new Map(exportItems);
|
||||
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
if (downloadUrl) {
|
||||
downloadExportedFile(downloadUrl);
|
||||
}
|
||||
break;
|
||||
case 'failed':
|
||||
await showErrorNotification(exportData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
exportItems = new Map();
|
||||
}
|
||||
|
||||
function graphSize(status: string): number {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 10;
|
||||
case 'processing':
|
||||
return 60;
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
return 100;
|
||||
default:
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
function text(status: string, tableName = '') {
|
||||
const table = tableName ? `<b>${tableName}</b>` : '';
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return `Exporting ${table} completed`;
|
||||
case 'failed':
|
||||
return `Exporting ${table} failed`;
|
||||
case 'processing':
|
||||
return `Exporting ${table}`;
|
||||
default:
|
||||
return 'Preparing export...';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
sdk.forProject(page.params.region, page.params.project)
|
||||
.migrations.list({
|
||||
queries: [
|
||||
Query.equal('destination', 'CSV'),
|
||||
Query.equal('status', ['pending', 'processing'])
|
||||
]
|
||||
})
|
||||
.then((migrations) => {
|
||||
migrations.migrations.forEach(updateOrAddItem);
|
||||
});
|
||||
|
||||
return realtime.forConsole(page.params.region, 'console', (response) => {
|
||||
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
|
||||
if (response.events.includes('migrations.*')) {
|
||||
updateOrAddItem(response.payload as Payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let isOpen = $state(true);
|
||||
let showCsvExportBox = $derived(exportItems.size > 0);
|
||||
let showErrorModal = $state(false);
|
||||
let selectedErrors = $state<string[]>([]);
|
||||
</script>
|
||||
|
||||
{#if showCsvExportBox}
|
||||
<Layout.Stack direction="column" gap="l" alignItems="flex-end">
|
||||
<section class="upload-box">
|
||||
<header class="upload-box-header">
|
||||
<h4 class="upload-box-title">
|
||||
<Typography.Text variant="m-500">
|
||||
Exporting rows ({exportItems.size})
|
||||
</Typography.Text>
|
||||
</h4>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
class:is-open={isOpen}
|
||||
aria-label="toggle upload box"
|
||||
onclick={() => (isOpen = !isOpen)}>
|
||||
<span class="icon-cheveron-up" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button class="upload-box-button" aria-label="close export box" onclick={clear}>
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="upload-box-content-list">
|
||||
{#each [...exportItems.entries()] as [key, value] (key)}
|
||||
<div class="upload-box-content" class:is-open={isOpen}>
|
||||
<ul class="upload-box-list">
|
||||
<li class="upload-box-item">
|
||||
<section class="progress-bar u-width-full-line">
|
||||
<div
|
||||
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<Typography.Text>
|
||||
{@html text(value.status, value.table)}
|
||||
</Typography.Text>
|
||||
{#if value.status === 'failed' && value.errors && value.errors.length > 0}
|
||||
<button
|
||||
class="link"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
selectedErrors = value.errors;
|
||||
showErrorModal = true;
|
||||
}}>
|
||||
more details
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar-container"
|
||||
class:is-danger={value.status === 'failed'}
|
||||
style="--graph-size:{graphSize(value.status)}%">
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<Modal bind:show={showErrorModal} title="Export error details" hideFooter>
|
||||
{#if selectedErrors.length > 0}
|
||||
<Code
|
||||
code={JSON.stringify(
|
||||
selectedErrors.map((err) => {
|
||||
try {
|
||||
return JSON.parse(err);
|
||||
} catch {
|
||||
return err;
|
||||
}
|
||||
}),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
lang="json"
|
||||
hideHeader />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.upload-box {
|
||||
display: flex;
|
||||
max-height: 320px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-box-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.upload-box-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.upload-box-content-list {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.upload-box-content {
|
||||
width: 304px;
|
||||
}
|
||||
|
||||
.upload-box-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 4px;
|
||||
|
||||
&::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-neutral-invert);
|
||||
}
|
||||
|
||||
&.is-danger::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-error);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,20 @@
|
||||
<script>
|
||||
import { getNextTier, tierToPlan } from '$lib/stores/billing';
|
||||
<script lang="ts">
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { getNextTier, tierToPlan } from '$lib/stores/billing';
|
||||
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let source = 'empty_state_card';
|
||||
export let responsive = false;
|
||||
|
||||
// type def for Layout.Stack!
|
||||
let direction: 'column' | 'row' | 'row-reverse' | 'column-reverse' = 'row';
|
||||
|
||||
$: direction = responsive ? ($isSmallViewport ? 'column' : 'row') : 'row';
|
||||
</script>
|
||||
|
||||
<Card.Base variant="secondary" padding="s" radius="s">
|
||||
<Layout.Stack direction="row" gap="l">
|
||||
<Layout.Stack {direction} gap="l">
|
||||
{#if $$slots?.image}
|
||||
<div style="flex-shrink:0">
|
||||
<slot name="image" />
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { InputDateTime, InputSelect } from '$lib/elements/forms';
|
||||
import { isSameDay, isValidDate, toLocaleDate } from '$lib/helpers/date';
|
||||
import { InputDate, InputSelect } from '$lib/elements/forms';
|
||||
import { isSameDay, isValidDate, toLocaleDate, toLocaleDateISO } from '$lib/helpers/date';
|
||||
|
||||
function incrementToday(value: number, type: 'day' | 'month' | 'year'): string {
|
||||
const date = new Date();
|
||||
@@ -74,6 +74,10 @@
|
||||
export let resourceType: string | 'key' | 'token' | undefined = 'key';
|
||||
export let expiryOptions: 'default' | 'limited' | ExpirationOptions[] = 'default';
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
let minDate: string = toLocaleDateISO(tomorrow.getTime());
|
||||
|
||||
const options = Array.isArray(expiryOptions)
|
||||
? expiryOptions
|
||||
: expiryOptions === 'default'
|
||||
@@ -129,6 +133,11 @@
|
||||
if (hasUserInteracted && !isSameDay(new Date(expirationSelect), new Date(value))) {
|
||||
value = expirationSelect === 'custom' ? expirationCustom : expirationSelect;
|
||||
}
|
||||
|
||||
// Only convert to ISO date if value is not null
|
||||
if (value !== null) {
|
||||
value = toLocaleDateISO(new Date(value).getTime());
|
||||
}
|
||||
}
|
||||
|
||||
$: helper =
|
||||
@@ -147,11 +156,11 @@
|
||||
on:change={() => (hasUserInteracted = true)} />
|
||||
|
||||
{#if expirationSelect === 'custom'}
|
||||
<InputDateTime
|
||||
<InputDate
|
||||
required
|
||||
type="date"
|
||||
id="expire"
|
||||
min={minDate}
|
||||
label={dateSelectorLabel}
|
||||
bind:value={expirationCustom}
|
||||
on:change={() => (hasUserInteracted = true)} />
|
||||
on:input={() => (hasUserInteracted = true)} />
|
||||
{/if}
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
repository.set(e);
|
||||
repositoryName = e.name;
|
||||
selectedRepository = e.id;
|
||||
connectRepo();
|
||||
}} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -13,6 +13,7 @@ export { default as Copy } from './copy.svelte';
|
||||
export { default as CopyInput } from './copyInput.svelte';
|
||||
export { default as UploadBox } from './uploadBox.svelte';
|
||||
export { default as BackupRestoreBox } from './backupRestoreBox.svelte';
|
||||
export { default as CsvExportBox } from './csvExportBox.svelte';
|
||||
export { default as List } from './list.svelte';
|
||||
export { default as ListItem } from './listItem.svelte';
|
||||
export { default as Empty } from './empty.svelte';
|
||||
|
||||
@@ -57,5 +57,6 @@
|
||||
/* temporary fix to modal width */
|
||||
:global(dialog section) {
|
||||
max-width: 100% !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
{#each [...$groups] as [role, permission] (role)}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="role" {root}>
|
||||
<Row {role} onNotFound={() => deleteRole(role)} />
|
||||
<Row {role} />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="create" {root}>
|
||||
<Selector.Checkbox
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
hideColumns?: boolean;
|
||||
allowNoColumns?: boolean;
|
||||
showAnyway?: boolean;
|
||||
disableButton?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -32,7 +33,8 @@
|
||||
hideView = false,
|
||||
hideColumns = false,
|
||||
allowNoColumns = false,
|
||||
showAnyway = false
|
||||
showAnyway = false,
|
||||
disableButton = false
|
||||
}: Props = $props();
|
||||
|
||||
let showCountBadge = $state(false);
|
||||
@@ -70,7 +72,7 @@
|
||||
icon={onlyIcon}
|
||||
onclick={toggle}
|
||||
variant="secondary"
|
||||
disabled={!$columns.length && showAnyway}
|
||||
disabled={(!$columns.length && showAnyway) || disableButton}
|
||||
class={onlyIcon && !$isSmallViewport ? 'width-fix' : undefined}
|
||||
badge={showCountBadge ? selectedColumnsNumber.toString() : undefined}>
|
||||
<Icon slot="start" icon={IconViewBoards} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const PAGE_LIMIT = 12; // default page limit
|
||||
export const SPREADSHEET_PAGE_LIMIT = 50; // default sheet page limit
|
||||
export const CARD_LIMIT = 6; // default card limit
|
||||
export const DEFAULT_BILLING_PROJECTS_LIMIT = 5; // default billing projects page limit
|
||||
export const INTERVAL = 5 * 60000; // default interval to check for feedback
|
||||
export const NEW_DEV_PRO_UPGRADE_COUPON = 'appw50';
|
||||
|
||||
@@ -24,6 +25,7 @@ export enum Dependencies {
|
||||
CREDIT = 'dependency:credit',
|
||||
INVOICES = 'dependency:invoices',
|
||||
ADDRESS = 'dependency:address',
|
||||
BILLING_AGGREGATION = 'dependency:billing_aggregation',
|
||||
UPGRADE_PLAN = 'dependency:upgrade_plan',
|
||||
ORGANIZATIONS = 'dependency:organizations',
|
||||
PAYMENT_METHODS = 'dependency:paymentMethods',
|
||||
@@ -253,14 +255,13 @@ export const scopes: {
|
||||
},
|
||||
{
|
||||
scope: 'indexes.read',
|
||||
description: "Access to read your project's database collection's indexes",
|
||||
description: "Access to read your project's database table's indexes",
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'indexes.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's database collection's indexes",
|
||||
description: "Access to create, update, and delete your project's database table's indexes",
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
indeterminate?: boolean;
|
||||
size?: 's' | 'm';
|
||||
description?: string;
|
||||
truncate?: boolean;
|
||||
}
|
||||
|
||||
export let id: string = '';
|
||||
@@ -22,6 +23,7 @@
|
||||
export let element: HTMLInputElement | undefined = undefined;
|
||||
export let size: $$Props['size'] = 's';
|
||||
export let description = '';
|
||||
export let truncate: boolean = false;
|
||||
let error: string;
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
@@ -50,6 +52,7 @@
|
||||
{label}
|
||||
{required}
|
||||
{description}
|
||||
{truncate}
|
||||
on:invalid={handleInvalid}
|
||||
on:click
|
||||
on:change />
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
onDeletePoint: (index: number) => void;
|
||||
onChangePoint: (pointIndex: number, coordIndex: number, newValue: number) => void;
|
||||
addLineButton?: Snippet;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -24,7 +25,8 @@
|
||||
onAddPoint,
|
||||
onDeletePoint,
|
||||
onChangePoint,
|
||||
addLineButton
|
||||
addLineButton,
|
||||
disabled
|
||||
}: Props = $props();
|
||||
|
||||
function isDeleteDisabled(index: number) {
|
||||
@@ -40,6 +42,7 @@
|
||||
<Layout.Stack>
|
||||
{#each values as value, index}
|
||||
<InputPoint
|
||||
{disabled}
|
||||
{nullable}
|
||||
values={value}
|
||||
deletePoints
|
||||
@@ -52,7 +55,11 @@
|
||||
|
||||
{#if values}
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center">
|
||||
<Button size="xs" compact on:click={() => onAddPoint(-1)} disabled={nullable}>
|
||||
<Button
|
||||
size="xs"
|
||||
compact
|
||||
on:click={() => onAddPoint(-1)}
|
||||
disabled={nullable || disabled}>
|
||||
<Icon icon={IconPlus} size="s" /> Add coordinate
|
||||
</Button>
|
||||
{@render addLineButton?.()}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
deletePoints?: boolean;
|
||||
onDeletePoint?: () => void;
|
||||
disableDelete?: boolean;
|
||||
disabled?: boolean;
|
||||
onChangePoint: (index: number, newValue: number) => void;
|
||||
}
|
||||
|
||||
@@ -21,7 +22,8 @@
|
||||
deletePoints = false,
|
||||
disableDelete = false,
|
||||
onDeletePoint,
|
||||
onChangePoint
|
||||
onChangePoint,
|
||||
disabled
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
@@ -38,6 +40,7 @@
|
||||
placeholder="Enter value"
|
||||
step={0.0001}
|
||||
value={values[index]}
|
||||
{disabled}
|
||||
on:change={(e) => onChangePoint(index, Number.parseFloat(`${e.detail}`))} />
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -45,7 +48,7 @@
|
||||
<Button
|
||||
size="s"
|
||||
secondary
|
||||
disabled={nullable || disableDelete}
|
||||
disabled={nullable || disableDelete || disabled}
|
||||
on:click={() => onDeletePoint?.()}>
|
||||
<Icon icon={IconX} size="s" />
|
||||
</Button>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
coordIndex: number,
|
||||
newValue: number
|
||||
) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -26,7 +27,8 @@
|
||||
onAddPoint,
|
||||
onAddLine,
|
||||
onDeletePoint,
|
||||
onChangePoint
|
||||
onChangePoint,
|
||||
disabled
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
@@ -34,6 +36,7 @@
|
||||
{#each values as value, index}
|
||||
<Layout.Stack gap="xs">
|
||||
<InputLine
|
||||
{disabled}
|
||||
values={value}
|
||||
onAddPoint={() => onAddPoint(index)}
|
||||
{nullable}
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
helper={error ?? helper}
|
||||
{required}
|
||||
state={error ? 'error' : 'default'}
|
||||
data-command-center-ignore
|
||||
on:invalid={handleInvalid}
|
||||
on:input
|
||||
on:change
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
/**
|
||||
* Build VCS repo URL from the template response model.
|
||||
* Example (GitHub): https://github.com/appwrite/templates-for-sites
|
||||
* build VCS repo URL from the template response model.
|
||||
* supports GitHub, GitLab, and Bitbucket.
|
||||
*
|
||||
* important: We use 'master' as the branch name because GitHub (and other providers)
|
||||
* redirect 'master' to the repository's default branch, regardless of whether
|
||||
* its actually named 'main', 'master', or something else. This ensures the
|
||||
* link works across all repositories without needing to know their default branch.
|
||||
*
|
||||
* Example (GitHub): https://github.com/appwrite/templates-for-sites/tree/master/sveltekit/starter
|
||||
*/
|
||||
export function getTemplateSourceUrl(
|
||||
t: Models.TemplateSite | Models.TemplateFunction
|
||||
@@ -20,7 +27,41 @@ export function getTemplateSourceUrl(
|
||||
bitbucket: 'bitbucket.org'
|
||||
};
|
||||
|
||||
const host = hostMap[provider.toLowerCase()] ?? provider; // fallback
|
||||
const host = hostMap[provider.toLowerCase()];
|
||||
if (!host) return null;
|
||||
|
||||
return `https://${host}/${owner}/${repo}`;
|
||||
let folderPath: string | undefined;
|
||||
if (
|
||||
'providerRootDirectory' in t &&
|
||||
t.providerRootDirectory &&
|
||||
typeof t.providerRootDirectory === 'string'
|
||||
) {
|
||||
folderPath = t.providerRootDirectory;
|
||||
} else if (
|
||||
'frameworks' in t &&
|
||||
t.frameworks?.length > 0 &&
|
||||
t.frameworks[0]?.providerRootDirectory &&
|
||||
typeof t.frameworks[0].providerRootDirectory === 'string'
|
||||
) {
|
||||
folderPath = t.frameworks[0].providerRootDirectory;
|
||||
}
|
||||
|
||||
let url = `https://${host}/${owner}/${repo}`;
|
||||
|
||||
if (folderPath) {
|
||||
const normalizedPath = folderPath.replace(/^\/+|\/+$/g, '');
|
||||
if (normalizedPath) {
|
||||
const providerLower = provider.toLowerCase();
|
||||
// Use 'master' as branch name - GitHub/GitLab/Bitbucket redirect it to default branch
|
||||
if (providerLower === 'github') {
|
||||
url = `${url}/tree/master/${normalizedPath}`;
|
||||
} else if (providerLower === 'gitlab') {
|
||||
url = `${url}/-/tree/master/${normalizedPath}`;
|
||||
} else if (providerLower === 'bitbucket') {
|
||||
url = `${url}/src/master/${normalizedPath}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
+16
-3
@@ -490,7 +490,7 @@ export class Billing {
|
||||
name: string,
|
||||
billingPlan: string,
|
||||
paymentMethodId: string,
|
||||
billingAddressId: string = null,
|
||||
billingAddressId: string = undefined,
|
||||
couponId: string = null,
|
||||
invites: Array<string> = [],
|
||||
budget: number = undefined,
|
||||
@@ -628,6 +628,7 @@ export class Billing {
|
||||
budget,
|
||||
taxId
|
||||
};
|
||||
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'patch',
|
||||
@@ -934,12 +935,24 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async getAggregation(organizationId: string, aggregationId: string): Promise<AggregationTeam> {
|
||||
async getAggregation(
|
||||
organizationId: string,
|
||||
aggregationId: string,
|
||||
limit?: number,
|
||||
offset?: number
|
||||
): Promise<AggregationTeam> {
|
||||
const path = `/organizations/${organizationId}/aggregations/${aggregationId}`;
|
||||
const params = {
|
||||
const params: {
|
||||
organizationId: string;
|
||||
aggregationId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} = {
|
||||
organizationId,
|
||||
aggregationId
|
||||
};
|
||||
if (typeof limit === 'number') params.limit = limit;
|
||||
if (typeof offset === 'number') params.offset = offset;
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'get',
|
||||
|
||||
@@ -100,7 +100,7 @@ export function tierToPlan(tier: Tier) {
|
||||
case BillingPlan.ENTERPRISE:
|
||||
return tierEnterprise;
|
||||
default:
|
||||
return tierFree;
|
||||
return tierCustom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +557,8 @@ export async function checkForMissingPaymentMethod() {
|
||||
const orgs = await sdk.forConsole.billing.listOrganization([
|
||||
Query.notEqual('billingPlan', BillingPlan.FREE),
|
||||
Query.isNull('paymentMethodId'),
|
||||
Query.isNull('backupPaymentMethodId')
|
||||
Query.isNull('backupPaymentMethodId'),
|
||||
Query.equal('platform', 'appwrite')
|
||||
]);
|
||||
if (orgs?.total) {
|
||||
orgMissingPaymentMethod.set(orgs.teams[0]);
|
||||
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
Tokens,
|
||||
TablesDB,
|
||||
Domains,
|
||||
Realtime
|
||||
Realtime,
|
||||
Organizations
|
||||
} from '@appwrite.io/console';
|
||||
import { Billing } from '../sdk/billing';
|
||||
import { Backups } from '../sdk/backups';
|
||||
@@ -95,7 +96,8 @@ function createConsoleSdk(client: Client) {
|
||||
sites: new Sites(client),
|
||||
domains: new Domains(client),
|
||||
storage: new Storage(client),
|
||||
realtime: new Realtime(client)
|
||||
realtime: new Realtime(client),
|
||||
organizations: new Organizations(client)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,51 @@
|
||||
<script>
|
||||
import { Container } from '$lib/layout';
|
||||
import { base } from '$app/paths';
|
||||
import { loading } from '$routes/store';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
loading.set(false);
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<slot />
|
||||
</Container>
|
||||
<Layout.Stack
|
||||
height="100vh"
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style="background: var(--bgcolor-neutral-primary, #fff);">
|
||||
<section class="console-container">
|
||||
<slot />
|
||||
</section>
|
||||
<footer>
|
||||
<Typography.Eyebrow color="--fgcolor-neutral-secondary">POWERED BY</Typography.Eyebrow>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src="{base}/images/appwrite-logo-dark.svg"
|
||||
width="120"
|
||||
height="22"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src="{base}/images/appwrite-logo-light.svg"
|
||||
width="120"
|
||||
height="22"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</footer>
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
footer {
|
||||
padding: 2rem 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { app } from '$lib/stores/app';
|
||||
import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg';
|
||||
import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg';
|
||||
import { Vcs, Client } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
export let data;
|
||||
const { data } = $props();
|
||||
|
||||
const endpoint = getApiEndpoint();
|
||||
const client = new Client();
|
||||
const vcs = new Vcs(client);
|
||||
|
||||
let installationId: string;
|
||||
let repositoryId: string;
|
||||
let providerPullRequestId: string;
|
||||
|
||||
let error = '';
|
||||
let success = '';
|
||||
let loading = false;
|
||||
let error = $state('');
|
||||
let success = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
repositoryId = data.repositoryId;
|
||||
installationId = data.installationId;
|
||||
providerPullRequestId = data.providerPullRequestId;
|
||||
|
||||
client.setEndpoint(endpoint).setProject(data.projectId).setMode('admin');
|
||||
});
|
||||
|
||||
@@ -39,9 +30,9 @@
|
||||
|
||||
try {
|
||||
await vcs.updateExternalDeployments({
|
||||
installationId,
|
||||
repositoryId,
|
||||
providerPullRequestId
|
||||
installationId: data.installationId,
|
||||
repositoryId: data.repositoryId,
|
||||
providerPullRequestId: data.providerPullRequestId
|
||||
});
|
||||
success = 'Deployment approved successfully! Build will start soon.';
|
||||
} catch (e) {
|
||||
@@ -52,49 +43,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="container" style="display: grid; place-items: center; min-height: 100vh;">
|
||||
<div class="u-flex u-flex-vertical u-cross-center" style="width: 100%">
|
||||
<div class="card" style="min-width: 600px; max-width: 100%;">
|
||||
<h1 class="heading-level-2">Authorize External Deployment</h1>
|
||||
<small style="margin-block-start: 8px;display: block;"
|
||||
>The deployment for pull request <code class="inline-code"
|
||||
>#{providerPullRequestId}</code> is awaiting approval. When authorized, deployments
|
||||
will be started.
|
||||
</small>
|
||||
|
||||
<div class="with-borders" style="margin-block-start: 1rem;display: block;">
|
||||
<button disabled={loading} on:click={approveDeployment} class="button" type="button"
|
||||
>Approve Deployment</button>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p style="margin-block-start: 1rem" class="u-color-text-danger">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<p style="margin-block-start: 1rem" class="u-color-text-success">{success}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-gap-4 u-flex u-main-center u-cross-center" style="margin-block-start: 2rem;">
|
||||
<span class="text">Powered by</span>
|
||||
<a
|
||||
href="https://appwrite.io/"
|
||||
target="_blank"
|
||||
style="display: grid;place-items: center;">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={AppwriteLogoDark}
|
||||
width="120"
|
||||
class="u-block u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="120"
|
||||
class="u-block u-only-light"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Layout.Stack gap="l" alignItems="center" style="max-width: 500px;">
|
||||
{#if success}
|
||||
<Badge type="success" variant="secondary" content={success} />
|
||||
{:else if error}
|
||||
<Badge type="error" variant="secondary" content={error} />
|
||||
{/if}
|
||||
<Typography.Title size="l" align="center">
|
||||
The deployment for pull request #{data.providerPullRequestId}
|
||||
is awaiting approval. When authorized, deployments will be started.
|
||||
</Typography.Title>
|
||||
<Button on:click={approveDeployment} secondary>Approve Deployment</Button>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageData } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Badge } from '@appwrite.io/pink-svelte';
|
||||
import { Badge, Skeleton } from '@appwrite.io/pink-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import { daysLeftInTrial, plansInfo, tierToPlan } from '$lib/stores/billing';
|
||||
import { daysLeftInTrial, plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -36,6 +36,27 @@
|
||||
return memberships.memberships.map((team) => team.userName || team.userEmail);
|
||||
}
|
||||
|
||||
async function getPlanName(billingPlan: string | undefined): Promise<string> {
|
||||
if (!billingPlan) return 'Unknown';
|
||||
|
||||
// For known plans, use tierToPlan
|
||||
const tierData = tierToPlan(billingPlan as Tier);
|
||||
|
||||
// If it's not a custom plan or we got a non-custom result, return the name
|
||||
if (tierData.name !== 'Custom') {
|
||||
return tierData.name;
|
||||
}
|
||||
|
||||
// For custom plans, fetch from API
|
||||
try {
|
||||
const plan = await sdk.forConsole.billing.getPlan(billingPlan);
|
||||
return plan.name;
|
||||
} catch (error) {
|
||||
// Fallback to 'Custom' if fetch fails
|
||||
return 'Custom';
|
||||
}
|
||||
}
|
||||
|
||||
function isOrganizationOnTrial(organization: Organization): boolean {
|
||||
if (!organization?.billingTrialStartDate) return false;
|
||||
if ($daysLeftInTrial <= 0) return false;
|
||||
@@ -92,6 +113,9 @@
|
||||
{#each data.organizations.teams as organization}
|
||||
{@const avatarList = getMemberships(organization.$id)}
|
||||
{@const payingOrg = isPayingOrganization(organization)}
|
||||
{@const planName = isCloudOrg(organization)
|
||||
? getPlanName(organization.billingPlan)
|
||||
: null}
|
||||
|
||||
<GridItem1 href={`${base}/organization-${organization.$id}`}>
|
||||
<svelte:fragment slot="eyebrow">
|
||||
@@ -104,16 +128,19 @@
|
||||
<svelte:fragment slot="status">
|
||||
{#if isCloudOrg(organization)}
|
||||
{#if isNonPayingOrganization(organization)}
|
||||
<Tooltip>
|
||||
<Badge
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
content={tierToPlan(organization?.billingPlan)?.name} />
|
||||
{#if planName}
|
||||
{#await planName}
|
||||
<Skeleton width={30} height={20} variant="line" />
|
||||
{:then name}
|
||||
<Tooltip>
|
||||
<Badge size="xs" variant="secondary" content={name} />
|
||||
|
||||
<span slot="tooltip">
|
||||
You are limited to 1 free organization per account
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span slot="tooltip">
|
||||
You are limited to 1 free organization per account
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/await}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isOrganizationOnTrial(organization)}
|
||||
@@ -132,16 +159,20 @@
|
||||
{/if}
|
||||
|
||||
{#if payingOrg}
|
||||
<Badge
|
||||
size="xs"
|
||||
type="success"
|
||||
variant="secondary"
|
||||
content={tierToPlan(payingOrg?.billingPlan)?.name} />
|
||||
{#await planName}
|
||||
<Skeleton width={30} height={20} variant="line" />
|
||||
{:then name}
|
||||
<Badge
|
||||
size="xs"
|
||||
type="success"
|
||||
variant="secondary"
|
||||
content={name} />
|
||||
{/await}
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#await avatarList}
|
||||
<span class="avatar is-color-empty"></span>
|
||||
<Skeleton width={40} height={40} variant="circle" />
|
||||
{:then avatars}
|
||||
<AvatarGroup {avatars} />
|
||||
{/await}
|
||||
|
||||
@@ -10,7 +10,12 @@ export const load: PageLoad = async ({ url, route }) => {
|
||||
const limit = getLimit(url, route, CARD_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
|
||||
const queries = [Query.offset(offset), Query.limit(limit), Query.orderDesc('')];
|
||||
const queries = [
|
||||
Query.offset(offset),
|
||||
Query.limit(limit),
|
||||
Query.orderDesc(''),
|
||||
Query.equal('platform', 'appwrite')
|
||||
];
|
||||
|
||||
const organizations = !isCloud
|
||||
? await sdk.forConsole.teams.list({ queries })
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
name,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
@@ -148,7 +148,7 @@
|
||||
selectedOrg.$id,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators
|
||||
);
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
ID.unique(),
|
||||
name,
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
} else {
|
||||
@@ -121,7 +120,7 @@
|
||||
name,
|
||||
selectedPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
undefined,
|
||||
selectedCoupon?.code,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
ID.unique(),
|
||||
organizationName,
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ export const load: PageLoad = async ({ parent }) => {
|
||||
ID.unique(),
|
||||
'Personal projects',
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
{#if activeProjects.length > 0}
|
||||
<CardContainer
|
||||
disableEmpty={!$canWriteProjects}
|
||||
total={data.projects.total}
|
||||
total={activeProjects.length}
|
||||
offset={data.offset}
|
||||
on:click={handleCreateProject}>
|
||||
{#each activeProjects as project}
|
||||
@@ -309,7 +309,7 @@
|
||||
name="Projects"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.projects.total} />
|
||||
total={activeProjects.length} />
|
||||
|
||||
<!-- Archived Projects Section -->
|
||||
<ArchiveProject
|
||||
|
||||
@@ -135,7 +135,9 @@
|
||||
availableCredit={data?.availableCredit}
|
||||
currentPlan={data?.currentPlan}
|
||||
nextPlan={data?.nextPlan}
|
||||
currentAggregation={data?.billingAggregation} />
|
||||
currentAggregation={data?.billingAggregation}
|
||||
limit={data?.limit}
|
||||
offset={data?.offset} />
|
||||
{:else}
|
||||
<PlanSummaryOld
|
||||
availableCredit={data?.availableCredit}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { BillingPlan, DEFAULT_BILLING_PROJECTS_LIMIT, Dependencies } from '$lib/constants';
|
||||
import type { Address } from '$lib/sdk/billing';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -7,7 +7,9 @@ import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export const load: PageLoad = async ({ parent, depends }) => {
|
||||
import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
|
||||
|
||||
export const load: PageLoad = async ({ parent, depends, url, route }) => {
|
||||
const { organization, scopes, currentPlan, countryList, locale } = await parent();
|
||||
|
||||
if (!scopes.includes('billing.read')) {
|
||||
@@ -19,6 +21,8 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
depends(Dependencies.CREDIT);
|
||||
depends(Dependencies.INVOICES);
|
||||
depends(Dependencies.ADDRESS);
|
||||
//aggregation reloads on page param changes
|
||||
depends(Dependencies.BILLING_AGGREGATION);
|
||||
|
||||
const billingAddressId = (organization as Organization)?.billingAddressId;
|
||||
const billingAddressPromise: Promise<Address> = billingAddressId
|
||||
@@ -34,9 +38,14 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
*/
|
||||
let billingAggregation = null;
|
||||
try {
|
||||
const currentPage = getPage(url) || 1;
|
||||
const limit = getLimit(url, route, DEFAULT_BILLING_PROJECTS_LIMIT);
|
||||
const offset = pageToOffset(currentPage, limit);
|
||||
billingAggregation = await sdk.forConsole.billing.getAggregation(
|
||||
organization.$id,
|
||||
(organization as Organization)?.billingAggregationId
|
||||
(organization as Organization)?.billingAggregationId,
|
||||
limit,
|
||||
offset
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
@@ -84,6 +93,11 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
areCreditsSupported,
|
||||
countryList,
|
||||
locale,
|
||||
nextPlan: billingPlanDowngrade
|
||||
nextPlan: billingPlanDowngrade,
|
||||
limit: getLimit(url, route, DEFAULT_BILLING_PROJECTS_LIMIT),
|
||||
offset: pageToOffset(
|
||||
getPage(url) || 1,
|
||||
getLimit(url, route, DEFAULT_BILLING_PROJECTS_LIMIT)
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
bind:selectedAddress={billingAddress} />
|
||||
{/if}
|
||||
{#if showReplace}
|
||||
<ReplaceAddress bind:show={showReplace} />
|
||||
<ReplaceAddress bind:show={showReplace} {locale} {countryList} />
|
||||
{/if}
|
||||
{#if showRemove}
|
||||
<RemoveAddress bind:show={showRemove} />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -199,7 +199,7 @@
|
||||
disabled={$organization?.markedForDeletion}
|
||||
href={$upgradeURL}
|
||||
on:click={() =>
|
||||
trackEvent('click_organization_plan_update', {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
source: 'billing_tab'
|
||||
})}>
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { base } from '$app/paths';
|
||||
import { Alert, Badge, Card, Layout, Skeleton } from '@appwrite.io/pink-svelte';
|
||||
import { page } from '$app/state';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let locale: Models.Locale;
|
||||
export let countryList: Models.CountryList;
|
||||
let loading = true;
|
||||
let addresses: AddressesList;
|
||||
let selectedAddress: string;
|
||||
@@ -44,13 +46,9 @@
|
||||
: null
|
||||
: null;
|
||||
|
||||
const locale = await sdk.forProject(page.params.region, page.params.project).locale.get();
|
||||
if (locale?.countryCode) {
|
||||
country = locale.countryCode;
|
||||
}
|
||||
const countryList = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.locale.listCountries();
|
||||
options = countryList.countries.map((country) => {
|
||||
return {
|
||||
value: country.code,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { WizardStepsType } from '$lib/layout/wizardWithSteps.svelte';
|
||||
import type { AggregationList, Invoice } from '$lib/sdk/billing';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import type { WizardStepsType } from '$lib/layout/wizardWithSteps.svelte';
|
||||
import type { AggregationList, Invoice, InvoiceUsage } from '$lib/sdk/billing';
|
||||
|
||||
export const aggregationList = derived(
|
||||
page,
|
||||
@@ -16,3 +16,31 @@ export const addCreditWizardStore = writable<{ coupon: string; paymentMethodId:
|
||||
|
||||
export const selectedInvoice = writable<Invoice>(null);
|
||||
export const showRetryModal = writable(false);
|
||||
|
||||
export type RowFactoryOptions = {
|
||||
id: string;
|
||||
label: string;
|
||||
resource?: InvoiceUsage;
|
||||
planLimit?: number | null;
|
||||
includeProgress?: boolean;
|
||||
formatValue?: (value: number | null | undefined) => string;
|
||||
usageFormatter?: (options: {
|
||||
value: number;
|
||||
planLimit?: number | null;
|
||||
resource?: InvoiceUsage;
|
||||
formatValue: (value: number | null | undefined) => string;
|
||||
hasLimit: boolean;
|
||||
}) => string;
|
||||
priceFormatter?: (options: { amount: number; resource?: InvoiceUsage }) => string;
|
||||
progressFactory?: (options: {
|
||||
value: number;
|
||||
planLimit?: number | null;
|
||||
resource?: InvoiceUsage;
|
||||
hasLimit: boolean;
|
||||
}) => Array<{ size: number; color: string; tooltip?: { title: string; label: string } }>;
|
||||
maxFactory?: (options: {
|
||||
planLimit?: number | null;
|
||||
hasLimit: boolean;
|
||||
resource?: InvoiceUsage;
|
||||
}) => number | null;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmPayment } from '$lib/stores/stripe';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { VARS } from '$lib/system';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import {
|
||||
Alert,
|
||||
@@ -140,30 +139,14 @@
|
||||
}
|
||||
|
||||
async function trackDowngradeFeedback() {
|
||||
const paidInvoices = await sdk.forConsole.billing.listInvoices(data.organization.$id, [
|
||||
Query.equal('status', 'succeeded'),
|
||||
Query.greaterThan('grossAmount', 0)
|
||||
]);
|
||||
|
||||
await fetch(`${VARS.GROWTH_ENDPOINT}/feedback/billing`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: tierToPlan(data.organization.billingPlan).name,
|
||||
to: tierToPlan(selectedPlan).name,
|
||||
email: data.account.email,
|
||||
reason: feedbackDowngradeOptions.find(
|
||||
(option) => option.value === feedbackDowngradeReason
|
||||
)?.label,
|
||||
orgId: data.organization.$id,
|
||||
userId: data.account.$id,
|
||||
orgAge: data.organization.$createdAt,
|
||||
userAge: data.account.$createdAt,
|
||||
paidInvoices: paidInvoices.total,
|
||||
message: feedbackMessage ?? ''
|
||||
})
|
||||
await sdk.forConsole.organizations.createDowngradeFeedback({
|
||||
organizationId: data.organization.$id,
|
||||
reason: feedbackDowngradeOptions.find(
|
||||
(option) => option.value === feedbackDowngradeReason
|
||||
)?.label,
|
||||
message: feedbackMessage ?? '',
|
||||
fromPlanId: data.organization.billingPlan,
|
||||
toPlanId: selectedPlan
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,8 +156,7 @@
|
||||
await sdk.forConsole.billing.updatePlan(
|
||||
data.organization.$id,
|
||||
selectedPlan,
|
||||
paymentMethodId,
|
||||
null
|
||||
paymentMethodId
|
||||
);
|
||||
|
||||
// 2) If the target plan has a project limit, apply selected projects now
|
||||
@@ -254,7 +236,7 @@
|
||||
data.organization.$id,
|
||||
selectedPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
undefined,
|
||||
selectedCoupon?.code,
|
||||
newCollaborators,
|
||||
billingBudget,
|
||||
|
||||
+2
-1
@@ -1,13 +1,14 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
|
||||
export const load = async ({ parent, depends }) => {
|
||||
depends(Dependencies.DOMAINS);
|
||||
|
||||
const organizations = !isCloud
|
||||
? await sdk.forConsole.teams.list()
|
||||
: await sdk.forConsole.billing.listOrganization();
|
||||
: await sdk.forConsole.billing.listOrganization([Query.equal('platform', 'appwrite')]);
|
||||
|
||||
const { domain } = await parent();
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { BackupRestoreBox, MigrationBox, UploadBox } from '$lib/components';
|
||||
import { BackupRestoreBox, MigrationBox, UploadBox, CsvExportBox } from '$lib/components';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { project, stats } from './store';
|
||||
@@ -119,6 +119,7 @@
|
||||
<MigrationBox />
|
||||
<BackupRestoreBox />
|
||||
<CsvImportBox />
|
||||
<CsvExportBox />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
+3
-2
@@ -38,8 +38,9 @@
|
||||
}
|
||||
};
|
||||
|
||||
$: secret =
|
||||
clientSecret && tenantID ? JSON.stringify({ clientSecret, tenantID }) : provider.secret;
|
||||
$: secret = clientSecret
|
||||
? JSON.stringify({ clientSecret, ...(tenantID && { tenantID }) })
|
||||
: provider.secret;
|
||||
</script>
|
||||
|
||||
<Modal {error} onSubmit={update} bind:show on:close title={`${provider.name} OAuth2 settings`}>
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@
|
||||
Learn more</Link.Anchor>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if isComponentDisabled}
|
||||
<EmptyCardImageCloud source="email_signature_card">
|
||||
<EmptyCardImageCloud responsive source="email_signature_card">
|
||||
<svelte:fragment slot="image">
|
||||
<div class=" is-only-mobile u-width-full-line u-height-100-percent">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
Enable or disable Appwrite branding in your email template signature.
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<EmptyCardImageCloud source="email_signature_card" let:nextTier>
|
||||
<EmptyCardImageCloud responsive source="email_signature_card" let:nextTier>
|
||||
<svelte:fragment slot="image">
|
||||
<div class=" is-only-mobile u-width-full-line u-height-100-percent">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import CreatePolicy from './database-[database]/backups/createPolicy.svelte';
|
||||
import { cronExpression, type UserBackupPolicy } from '$lib/helpers/backups';
|
||||
@@ -132,7 +131,7 @@
|
||||
<CustomId bind:show={showCustomId} name="Database" bind:id autofocus={false} />
|
||||
|
||||
{#if isCloud}
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE}
|
||||
{#if !$currentPlan?.backupsEnabled}
|
||||
<Alert.Inline title="This database won't be backed up" status="warning">
|
||||
Upgrade your plan to ensure your data stays safe and backed up.
|
||||
<svelte:fragment slot="actions">
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import Input from './input.svelte';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { tableColumnSuggestions } from './store';
|
||||
|
||||
let {
|
||||
show = $bindable(false)
|
||||
}: {
|
||||
show?: boolean;
|
||||
} = $props();
|
||||
|
||||
const isOnRowsPage = $derived(page.route?.id?.endsWith('table-[table]'));
|
||||
|
||||
function resetSuggestionsStore() {
|
||||
show = false;
|
||||
|
||||
$tableColumnSuggestions.table = null;
|
||||
$tableColumnSuggestions.context = null;
|
||||
|
||||
$tableColumnSuggestions.force = false;
|
||||
$tableColumnSuggestions.enabled = false;
|
||||
$tableColumnSuggestions.thinking = false;
|
||||
}
|
||||
|
||||
async function triggerColumnSuggestions() {
|
||||
// set table info. first!
|
||||
$tableColumnSuggestions.table = {
|
||||
id: page.params.table,
|
||||
name: page.data.table?.name ?? 'Table'
|
||||
};
|
||||
|
||||
if (!isOnRowsPage) {
|
||||
await goto(
|
||||
resolve(
|
||||
'/(console)/project-[region]-[project]/databases/database-[database]/table-[table]',
|
||||
{
|
||||
region: page.params.region,
|
||||
project: page.params.project,
|
||||
database: page.params.database,
|
||||
table: page.params.table
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$tableColumnSuggestions.force = true;
|
||||
$tableColumnSuggestions.enabled = true;
|
||||
|
||||
show = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:show title="Suggest columns" onSubmit={triggerColumnSuggestions}>
|
||||
<Input isModal />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={resetSuggestionsStore}>Cancel</Button>
|
||||
<Button submit>Generate columns</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
+1527
-312
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -71,12 +71,15 @@
|
||||
border: 1.25px solid rgba(253, 54, 110, 0.12);
|
||||
|
||||
padding: 5px 0;
|
||||
min-width: 40px;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
:global(.ai-icon-holder.notification) {
|
||||
width: 36px !important;
|
||||
height: 32px !important;
|
||||
& svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
</script>
|
||||
|
||||
<Layout.Stack
|
||||
inline
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style="width: 18px !important; height: 20px !important;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5.00049 2C5.55277 2 6.00049 2.44772 6.00049 3V4H7.00049C7.55277 4 8.00049 4.44772 8.00049 5C8.00049 5.55228 7.55277 6 7.00049 6H6.00049V7C6.00049 7.55228 5.55277 8 5.00049 8C4.4482 8 4.00049 7.55228 4.00049 7V6H3.00049C2.4482 6 2.00049 5.55228 2.00049 5C2.00049 4.44772 2.4482 4 3.00049 4H4.00049V3C4.00049 2.44772 4.4482 2 5.00049 2ZM5.00049 12C5.55277 12 6.00049 12.4477 6.00049 13V14H7.00049C7.55277 14 8.00049 14.4477 8.00049 15C8.00049 15.5523 7.55277 16 7.00049 16H6.00049V17C6.00049 17.5523 5.55277 18 5.00049 18C4.4482 18 4.00049 17.5523 4.00049 17V16H3.00049C2.4482 16 2.00049 15.5523 2.00049 15C2.00049 14.4477 2.4482 14 3.00049 14H4.00049V13C4.00049 12.4477 4.4482 12 5.00049 12Z"
|
||||
fill="#97979B" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.0004 2C12.4542 2 12.851 2.30548 12.9671 2.74411L14.1464 7.19893L17.5002 9.13381C17.8097 9.3124 18.0004 9.64262 18.0004 10C18.0004 10.3574 17.8097 10.6876 17.5002 10.8662L14.1464 12.8011L12.9671 17.2559C12.851 17.6945 12.4542 18 12.0004 18C11.5467 18 11.1498 17.6945 11.0337 17.2559L9.85451 12.8011L6.50076 10.8662C6.19121 10.6876 6.00049 10.3574 6.00049 10C6.00049 9.64262 6.19121 9.31241 6.50076 9.13382L9.85451 7.19893L11.0337 2.74411C11.1498 2.30548 11.5467 2 12.0004 2Z"
|
||||
fill="#97979B" />
|
||||
</svg>
|
||||
</Layout.Stack>
|
||||
+3
-26
@@ -8,7 +8,7 @@
|
||||
mockSuggestions,
|
||||
type SuggestedIndexSchema
|
||||
} from './store';
|
||||
import { Modal, Confirm } from '$lib/components';
|
||||
import { Modal } from '$lib/components';
|
||||
import SideSheet from '../table-[table]/layout/sidesheet.svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { IndexType, type Models } from '@appwrite.io/console';
|
||||
@@ -32,7 +32,6 @@
|
||||
let creatingIndexes = $state(false);
|
||||
let loadingSuggestions = $state(false);
|
||||
let indexes = $state<SuggestedIndexSchema[]>([]);
|
||||
let confirmDismiss = $state(false);
|
||||
let columnOptions: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
@@ -195,7 +194,6 @@
|
||||
|
||||
function dismissIndexes() {
|
||||
indexes = [];
|
||||
confirmDismiss = false;
|
||||
$showIndexesSuggestions = false;
|
||||
}
|
||||
|
||||
@@ -354,13 +352,7 @@
|
||||
text
|
||||
size="s"
|
||||
disabled={loadingSuggestions || creatingIndexes}
|
||||
on:click={() => {
|
||||
if (indexes.length > 0 && !creatingIndexes) {
|
||||
confirmDismiss = true;
|
||||
} else {
|
||||
$showIndexesSuggestions = false;
|
||||
}
|
||||
}}>Cancel</Button>
|
||||
on:click={() => dismissIndexes()}>Cancel</Button>
|
||||
|
||||
<Button
|
||||
size="s"
|
||||
@@ -389,13 +381,7 @@
|
||||
}}
|
||||
cancel={{
|
||||
disabled: loadingSuggestions || creatingIndexes,
|
||||
onClick: () => {
|
||||
if (indexes.length > 0 && !creatingIndexes) {
|
||||
confirmDismiss = true;
|
||||
} else {
|
||||
$showIndexesSuggestions = false;
|
||||
}
|
||||
}
|
||||
onClick: () => dismissIndexes()
|
||||
}}>
|
||||
{#if modalError}
|
||||
<Alert.Inline status="error" title={modalError} />
|
||||
@@ -540,15 +526,6 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<Confirm
|
||||
confirmDeletion
|
||||
action="Dismiss"
|
||||
title="Dismiss indexes"
|
||||
bind:open={confirmDismiss}
|
||||
onSubmit={dismissIndexes}>
|
||||
Are you sure you want to dismiss these suggested indexes? This action cannot be undone.
|
||||
</Confirm>
|
||||
|
||||
<style lang="scss">
|
||||
// Custom logic to hide the Sheet's
|
||||
// `X` close button (not configurable via props)
|
||||
|
||||
+15
-3
@@ -7,6 +7,12 @@
|
||||
import { Button, InputTextarea } from '$lib/elements/forms';
|
||||
import { Card, Layout, Selector, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const {
|
||||
isModal = false
|
||||
}: {
|
||||
isModal?: boolean;
|
||||
} = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (featureActive) {
|
||||
$tableColumnSuggestions.enabled = true;
|
||||
@@ -23,7 +29,9 @@
|
||||
|
||||
const subtitle = $derived.by(() => {
|
||||
return featureActive
|
||||
? 'Enable AI to suggest useful columns based on your table name'
|
||||
? isModal
|
||||
? 'Use AI to suggest useful columns'
|
||||
: 'Enable AI to suggest useful columns based on your table name'
|
||||
: 'Sign up for Cloud to generate columns based on your table name';
|
||||
});
|
||||
</script>
|
||||
@@ -42,7 +50,7 @@
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
{#if featureActive}
|
||||
{#if featureActive && !isModal}
|
||||
<div class="suggestions-switch">
|
||||
<Selector.Switch
|
||||
id="suggestions"
|
||||
@@ -62,7 +70,7 @@
|
||||
|
||||
<!-- just being safe with extra guard! -->
|
||||
{#if $tableColumnSuggestions.enabled && featureActive}
|
||||
<div transition:slide={{ duration: 200 }}>
|
||||
<div class="context-input" transition:slide={{ duration: 200 }}>
|
||||
<InputTextarea
|
||||
id="context"
|
||||
rows={3}
|
||||
@@ -78,4 +86,8 @@
|
||||
.suggestions-switch :global(button):not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.context-input :global(.input) {
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
+33
-5
@@ -1,21 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Popover } from '@appwrite.io/pink-svelte';
|
||||
import { Popover, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import SideSheet from '../table-[table]/layout/sidesheet.svelte';
|
||||
|
||||
let {
|
||||
children,
|
||||
tooltipChildren,
|
||||
mobileFooterChildren,
|
||||
toggleOnTapClick = true,
|
||||
onShowStateChanged = null,
|
||||
enabled = true
|
||||
enabled = true,
|
||||
onChildrenClick,
|
||||
triggerOpen,
|
||||
headerTooltipText
|
||||
}: {
|
||||
children: Snippet<[toggle: (event: Event) => void]>;
|
||||
tooltipChildren: Snippet<[toggle: (event: Event) => void]>;
|
||||
mobileFooterChildren?: Snippet<[toggle: (event: Event) => void]>;
|
||||
toggleOnTapClick?: boolean;
|
||||
onShowStateChanged?: (showing: boolean) => void;
|
||||
enabled?: boolean;
|
||||
onChildrenClick?: () => void;
|
||||
triggerOpen?: () => boolean;
|
||||
headerTooltipText?: string;
|
||||
} = $props();
|
||||
|
||||
let showSheet = $state(false);
|
||||
@@ -25,6 +33,12 @@
|
||||
showSheet = false;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($isSmallViewport && triggerOpen && triggerOpen()) {
|
||||
showSheet = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover let:toggle let:showing portal padding="none" placement="bottom-start">
|
||||
@@ -36,9 +50,19 @@
|
||||
{@render children(() => (showSheet = false))}
|
||||
</button>
|
||||
{:else}
|
||||
<button style:cursor={enabled ? 'pointer' : undefined}>
|
||||
{@render children(toggle)}
|
||||
</button>
|
||||
<div style:display="grid">
|
||||
<Tooltip maxWidth="225px" portal disabled={!headerTooltipText || showing} delay={100}>
|
||||
<button
|
||||
onclick={() => enabled && onChildrenClick?.()}
|
||||
style:cursor={enabled ? 'pointer' : undefined}>
|
||||
{@render children(toggle)}
|
||||
</button>
|
||||
|
||||
<svelte:fragment slot="tooltip">
|
||||
{headerTooltipText}
|
||||
</svelte:fragment>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div let:toggle slot="tooltip" style:width="480px" style:padding="16px">
|
||||
@@ -56,6 +80,10 @@
|
||||
showSheet = false;
|
||||
}
|
||||
}}>
|
||||
{#snippet footer()}
|
||||
{@render mobileFooterChildren?.(() => (showSheet = false))}
|
||||
{/snippet}
|
||||
|
||||
{@render tooltipChildren(() => (showSheet = false))}
|
||||
</SideSheet>
|
||||
{/if}
|
||||
|
||||
+22
-7
@@ -3,6 +3,7 @@ import { IndexType } from '@appwrite.io/console';
|
||||
import { columnOptions } from '../table-[table]/columns/store';
|
||||
|
||||
export type TableColumnSuggestions = {
|
||||
force: boolean;
|
||||
enabled: boolean;
|
||||
thinking: boolean;
|
||||
context?: string | undefined;
|
||||
@@ -18,11 +19,15 @@ export type SuggestedColumnSchema = {
|
||||
key: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
array?: boolean;
|
||||
default?: string | number | boolean | number[] | number[][] | number[][][] | null;
|
||||
size?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
format?: string | null;
|
||||
encrypt?: boolean | null;
|
||||
elements?: string[];
|
||||
isPlaceholder?: boolean;
|
||||
};
|
||||
|
||||
export enum IndexOrder {
|
||||
@@ -43,11 +48,14 @@ export const tableColumnSuggestions = writable<TableColumnSuggestions>({
|
||||
enabled: false,
|
||||
context: null,
|
||||
thinking: false,
|
||||
table: null
|
||||
table: null,
|
||||
force: false
|
||||
});
|
||||
|
||||
export const showIndexesSuggestions = writable<boolean>(false);
|
||||
|
||||
export const showColumnsSuggestionsModal = writable<boolean>(false);
|
||||
|
||||
export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
|
||||
total: 7,
|
||||
columns: [
|
||||
@@ -68,7 +76,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
|
||||
formatOptions: null
|
||||
},
|
||||
{
|
||||
name: 'publishedYear',
|
||||
name: 'year',
|
||||
type: 'integer',
|
||||
size: null,
|
||||
format: null,
|
||||
@@ -79,7 +87,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'genre',
|
||||
name: 'category',
|
||||
type: 'string',
|
||||
size: 64,
|
||||
format: null,
|
||||
@@ -88,7 +96,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
|
||||
default: null
|
||||
},
|
||||
{
|
||||
name: 'isbn',
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
size: 13,
|
||||
required: false,
|
||||
@@ -96,7 +104,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
|
||||
default: null
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
name: 'spokenLanguage',
|
||||
type: 'string',
|
||||
size: 32,
|
||||
format: null,
|
||||
@@ -105,7 +113,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
|
||||
default: null
|
||||
},
|
||||
{
|
||||
name: 'pageCount',
|
||||
name: 'count',
|
||||
type: 'integer',
|
||||
required: false,
|
||||
min: 1,
|
||||
@@ -123,9 +131,11 @@ export type ColumnInput = {
|
||||
min?: number;
|
||||
max?: number;
|
||||
format?: string;
|
||||
elements?: string[];
|
||||
formatOptions?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
elements?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -134,6 +144,7 @@ export function mapSuggestedColumns<T extends ColumnInput>(columns: T[]): Sugges
|
||||
key: col.name,
|
||||
type: col.type,
|
||||
required: col.required ?? false,
|
||||
array: false,
|
||||
default: col.default ?? null,
|
||||
size: col.type === 'string' ? (col.size ?? undefined) : undefined,
|
||||
min:
|
||||
@@ -144,7 +155,11 @@ export function mapSuggestedColumns<T extends ColumnInput>(columns: T[]): Sugges
|
||||
col.type === 'integer' || col.type === 'double'
|
||||
? (col.max ?? col.formatOptions?.max ?? undefined)
|
||||
: undefined,
|
||||
format: col.format ?? null
|
||||
format: col.format ?? null,
|
||||
elements:
|
||||
col.format === 'enum'
|
||||
? (col.elements ?? col.formatOptions?.elements ?? undefined)
|
||||
: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -5,8 +5,6 @@
|
||||
import { Badge, Icon, Layout, Tag, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
export let isFlex = true;
|
||||
export let title: string;
|
||||
@@ -50,7 +48,7 @@
|
||||
paddingBlock="var(--space-5, 12px)"
|
||||
paddingInline="var(--space-6, 16px)"
|
||||
resetListPadding>
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO}
|
||||
{#if maxPolicies === 1}
|
||||
<Tag
|
||||
size="s"
|
||||
style="white-space: nowrap; max-width: none;"
|
||||
|
||||
+4
-5
@@ -20,8 +20,7 @@
|
||||
type UserBackupPolicy
|
||||
} from '$lib/helpers/backups';
|
||||
import { InputNumber } from '$lib/elements/forms/index.js';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { currentPlan } from '$lib/stores/organization';
|
||||
import { Card, Icon, Layout, Link, Tag, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconPencil, IconTrash } from '@appwrite.io/pink-icons-svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
@@ -149,7 +148,7 @@
|
||||
);
|
||||
|
||||
// pre-check the hourly if on pro plan
|
||||
if ($organization.billingPlan === BillingPlan.PRO && isFromBackupsTab) {
|
||||
if ($currentPlan?.backupPolicies === 1 && isFromBackupsTab) {
|
||||
presetPolicies.update((all) =>
|
||||
all.map((policy) => {
|
||||
policy.id = ID.unique();
|
||||
@@ -176,7 +175,7 @@
|
||||
</script>
|
||||
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
{#if $organization.billingPlan === BillingPlan.SCALE}
|
||||
{#if $currentPlan?.backupPolicies > 1}
|
||||
{#if title || subtitle}
|
||||
<div class="body-text-2">
|
||||
{#if title}
|
||||
@@ -195,7 +194,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- because we show a set of pre-defined ones -->
|
||||
{#if $organization.billingPlan === BillingPlan.PRO}
|
||||
{#if $currentPlan?.backupPolicies === 1}
|
||||
{@const dailyPolicy = $presetPolicies[1]}
|
||||
|
||||
{#if isFromBackupsTab}
|
||||
|
||||
+4
@@ -74,6 +74,8 @@
|
||||
|
||||
import { isTabletViewport } from '$lib/stores/viewport';
|
||||
import IndexesSuggestions from '../(suggestions)/indexes.svelte';
|
||||
import ColumnsSuggestions from '../(suggestions)/columns.svelte';
|
||||
import { showColumnsSuggestionsModal } from '../(suggestions)';
|
||||
|
||||
let editRow: EditRow;
|
||||
let editRelatedRow: EditRelatedRow;
|
||||
@@ -608,6 +610,8 @@
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
|
||||
<ColumnsSuggestions bind:show={$showColumnsSuggestionsModal} />
|
||||
|
||||
<IndexesSuggestions />
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
+176
-67
@@ -2,11 +2,13 @@
|
||||
import { Filters, hasPageQueries, queries } from '$lib/components/filters';
|
||||
import ViewSelector from '$lib/components/viewSelector.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { Column, ColumnType } from '$lib/helpers/types';
|
||||
import { Container } from '$lib/layout';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
|
||||
import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Layout, Divider, Tooltip, Typography, Link } from '@appwrite.io/pink-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
table,
|
||||
@@ -27,19 +29,29 @@
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import {
|
||||
IconBookOpen,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconPlus,
|
||||
IconRefresh
|
||||
IconViewBoards,
|
||||
IconRefresh,
|
||||
IconUpload,
|
||||
IconDownload
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import EmptySheet from './layout/emptySheet.svelte';
|
||||
import CreateRow from './rows/create.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import {
|
||||
Empty as SuggestionsEmptySheet,
|
||||
tableColumnSuggestions,
|
||||
showColumnsSuggestionsModal
|
||||
} from '../(suggestions)';
|
||||
import EmptySheetCards from './layout/emptySheetCards.svelte';
|
||||
import IconAI from '../(suggestions)/icon/aiForButton.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -91,13 +103,15 @@
|
||||
$tableColumnSuggestions.table &&
|
||||
$tableColumnSuggestions.table.id === page.params.table;
|
||||
|
||||
$: disableButton = canShowSuggestionsSheet;
|
||||
|
||||
async function onSelect(file: Models.File, localFile = false) {
|
||||
$isCsvImportInProgress = true;
|
||||
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.migrations.createCsvMigration({
|
||||
.migrations.createCSVImport({
|
||||
bucketId: file.bucketId,
|
||||
fileId: file.$id,
|
||||
resourceId: `${page.params.database}:${page.params.table}`,
|
||||
@@ -121,6 +135,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getTableExportUrl() {
|
||||
const queryParam = page.url.searchParams.get('query');
|
||||
const url = resolve(
|
||||
'/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export',
|
||||
{
|
||||
region: page.params.region,
|
||||
project: page.params.project,
|
||||
database: page.params.database,
|
||||
table: page.params.table
|
||||
}
|
||||
);
|
||||
return queryParam ? `${url}?query=${encodeURIComponent(queryParam)}` : url;
|
||||
}
|
||||
|
||||
onDestroy(() => ($showCreateColumnSheet.show = false));
|
||||
</script>
|
||||
|
||||
@@ -138,7 +166,8 @@
|
||||
columns={tableColumns}
|
||||
hideView
|
||||
showAnyway
|
||||
isCustomTable />
|
||||
isCustomTable
|
||||
{disableButton} />
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="tooltip">Columns</svelte:fragment>
|
||||
@@ -149,7 +178,7 @@
|
||||
onlyIcon
|
||||
query={data.query}
|
||||
columns={filterColumns}
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
disabled={!(hasColumns && hasValidColumns) || disableButton}
|
||||
analyticsSource="database_tables" />
|
||||
|
||||
<svelte:fragment slot="tooltip">Filters</svelte:fragment>
|
||||
@@ -160,37 +189,52 @@
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
style="padding-right: 40px;">
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<Button
|
||||
secondary
|
||||
event={Click.DatabaseImportCsv}
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
on:click={() => (showImportCSV = true)}>
|
||||
Import CSV
|
||||
</Button>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
gap="s">
|
||||
{#if !$isSmallViewport}
|
||||
<Button
|
||||
secondary
|
||||
event="create_row"
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
disabled={!(hasColumns && hasValidColumns) || disableButton}
|
||||
on:click={() => ($showRowCreateSheet.show = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create row
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
class="small-button-dimensions"
|
||||
on:click={() => {
|
||||
$expandTabs = !$expandTabs;
|
||||
preferences.setKey('tableHeaderExpanded', $expandTabs);
|
||||
}}>
|
||||
<Icon
|
||||
icon={!$expandTabs ? IconChevronDown : IconChevronUp}
|
||||
size="s" />
|
||||
</Button>
|
||||
<Tooltip placement="top">
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
class="small-button-dimensions"
|
||||
disabled={!(hasColumns && hasValidColumns) || disableButton}
|
||||
on:click={() => (showImportCSV = true)}>
|
||||
<Icon icon={IconUpload} size="s" />
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="tooltip">Import CSV</svelte:fragment>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip placement="top">
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
class="small-button-dimensions"
|
||||
disabled={!(hasColumns && hasValidColumns && data.rows.total) ||
|
||||
disableButton}
|
||||
on:click={() => {
|
||||
trackEvent(Click.DatabaseExportCsv);
|
||||
goto(getTableExportUrl());
|
||||
}}>
|
||||
<Icon icon={IconDownload} size="s" />
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="tooltip">Export CSV</svelte:fragment>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip disabled={isRefreshing || !data.rows.total} placement="top">
|
||||
<Button
|
||||
@@ -199,7 +243,8 @@
|
||||
secondary
|
||||
disabled={isRefreshing ||
|
||||
!data.rows.total ||
|
||||
!(hasColumns && hasValidColumns)}
|
||||
!(hasColumns && hasValidColumns) ||
|
||||
disableButton}
|
||||
class="small-button-dimensions"
|
||||
on:click={async () => {
|
||||
isRefreshing = true;
|
||||
@@ -213,6 +258,25 @@
|
||||
|
||||
<svelte:fragment slot="tooltip">Refresh</svelte:fragment>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip placement="top">
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
class="small-button-dimensions"
|
||||
on:click={() => {
|
||||
$expandTabs = !$expandTabs;
|
||||
preferences.setKey('tableHeaderExpanded', $expandTabs);
|
||||
}}>
|
||||
<Icon
|
||||
icon={!$expandTabs ? IconChevronDown : IconChevronUp}
|
||||
size="s" />
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="tooltip"
|
||||
>{!$expandTabs ? 'Expand' : 'Collapse'}</svelte:fragment>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
@@ -221,7 +285,7 @@
|
||||
<Button
|
||||
secondary
|
||||
event="create_row"
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
disabled={!(hasColumns && hasValidColumns) || disableButton}
|
||||
on:click={() => ($showRowCreateSheet.show = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create row
|
||||
@@ -231,7 +295,7 @@
|
||||
</Container>
|
||||
|
||||
<div class="databases-spreadsheet">
|
||||
{#if hasColumns && hasValidColumns}
|
||||
{#if hasColumns && hasValidColumns && $tableColumnSuggestions.force !== true}
|
||||
{#if data.rows.total}
|
||||
<Divider />
|
||||
<SpreadSheet {data} bind:showRowCreateSheet={$showRowCreateSheet} />
|
||||
@@ -239,58 +303,102 @@
|
||||
<EmptySheet
|
||||
mode="rows-filtered"
|
||||
title="There are no rows that match your filters"
|
||||
customColumns={createTableColumns($table.columns, selected)}
|
||||
actions={{
|
||||
primary: {
|
||||
text: 'Clear filters',
|
||||
onClick: () => {
|
||||
customColumns={createTableColumns($table.columns, selected)}>
|
||||
{#snippet actions()}
|
||||
<Button
|
||||
size="s"
|
||||
secondary
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
trackEvent(Submit.FilterClear, {
|
||||
source: 'database_tables'
|
||||
});
|
||||
}
|
||||
}
|
||||
}} />
|
||||
}}>
|
||||
Clear filters
|
||||
</Button>
|
||||
{/snippet}
|
||||
</EmptySheet>
|
||||
{:else}
|
||||
<EmptySheet
|
||||
mode="rows"
|
||||
customColumns={createTableColumns($table.columns, selected)}
|
||||
showActions={$canWriteRows}
|
||||
actions={{
|
||||
primary: {
|
||||
text: 'Create rows',
|
||||
onClick: () => {
|
||||
customColumns={createTableColumns($table.columns, selected)}>
|
||||
{#snippet actions()}
|
||||
<EmptySheetCards
|
||||
icon={IconPlus}
|
||||
title="Create rows"
|
||||
subtitle="Create rows manually"
|
||||
onClick={() => {
|
||||
$showRowCreateSheet.show = true;
|
||||
}
|
||||
},
|
||||
random: {
|
||||
onClick: () => {
|
||||
}} />
|
||||
|
||||
<EmptySheetCards
|
||||
icon={IconViewBoards}
|
||||
title="Generate sample data"
|
||||
subtitle="Generate data for testing"
|
||||
onClick={() => {
|
||||
$randomDataModalState.show = true;
|
||||
}
|
||||
}
|
||||
}} />
|
||||
}} />
|
||||
{/snippet}
|
||||
</EmptySheet>
|
||||
{/if}
|
||||
{:else if isCloud && canShowSuggestionsSheet}
|
||||
<SuggestionsEmptySheet />
|
||||
<SuggestionsEmptySheet userColumns={$tableColumns} userDataRows={data.rows.rows} />
|
||||
{:else}
|
||||
<EmptySheet
|
||||
mode="rows"
|
||||
title="You have no columns yet"
|
||||
showActions={$canWriteTables}
|
||||
actions={{
|
||||
primary: {
|
||||
text: 'Create column',
|
||||
onClick: async () => {
|
||||
<EmptySheet mode="rows" showActions={$canWriteTables} title="You have no columns yet">
|
||||
{#snippet subtitle()}
|
||||
{#if !isCloud}
|
||||
<!-- shown on self-hosted -->
|
||||
<Typography.Text align="center">
|
||||
Need a hand? Learn more in the
|
||||
<Link.Anchor
|
||||
target="_blank"
|
||||
href="https://appwrite.io/docs/products/databases">
|
||||
docs.
|
||||
</Link.Anchor>
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet actions()}
|
||||
{#if isCloud}
|
||||
<!-- shown on cloud -->
|
||||
<EmptySheetCards
|
||||
icon={IconAI}
|
||||
title="Suggest columns"
|
||||
subtitle="Use AI to generate columns"
|
||||
onClick={() => {
|
||||
$showColumnsSuggestionsModal = true;
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
<EmptySheetCards
|
||||
icon={IconPlus}
|
||||
title="Create column"
|
||||
subtitle="Create columns manually"
|
||||
onClick={() => {
|
||||
$showCreateColumnSheet.show = true;
|
||||
}
|
||||
},
|
||||
random: {
|
||||
onClick: () => {
|
||||
}} />
|
||||
|
||||
<EmptySheetCards
|
||||
icon={IconViewBoards}
|
||||
title="Generate sample data"
|
||||
subtitle="Generate data for testing"
|
||||
onClick={() => {
|
||||
$randomDataModalState.show = true;
|
||||
}
|
||||
}
|
||||
}} />
|
||||
}} />
|
||||
|
||||
{#if isCloud}
|
||||
<!-- shown on cloud because self-hosted shows a link above -->
|
||||
<EmptySheetCards
|
||||
icon={IconBookOpen}
|
||||
title="Documentation"
|
||||
subtitle="Read the Appwrite docs"
|
||||
href="https://appwrite.io/docs/products/databases" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EmptySheet>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
@@ -323,6 +431,7 @@
|
||||
|
||||
:global(.rotating) {
|
||||
animation: rotate 1s linear infinite;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
|
||||
+3
-8
@@ -382,8 +382,9 @@
|
||||
{column.key}{column.array ? '[]' : undefined}
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
|
||||
{#if isString(column) && column.encrypt}
|
||||
<Tooltip>
|
||||
<Tooltip portal>
|
||||
<Icon
|
||||
size="s"
|
||||
icon={IconLockClosed}
|
||||
@@ -391,13 +392,7 @@
|
||||
<div slot="tooltip">Encrypted</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Layout.Stack
|
||||
gap="s"
|
||||
inline
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
style="flex:0 0 auto; white-space:nowrap;">
|
||||
|
||||
{#if column.status !== 'available'}
|
||||
<Badge
|
||||
size="s"
|
||||
|
||||
+7
-2
@@ -42,6 +42,7 @@
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnBoolean> = {
|
||||
required: false,
|
||||
array: false,
|
||||
@@ -77,7 +78,7 @@
|
||||
id="default"
|
||||
label="Default value"
|
||||
placeholder="Select a value"
|
||||
disabled={data.required || data.array}
|
||||
disabled={data.required || data.array || disabled}
|
||||
options={[
|
||||
{ label: 'NULL', value: null },
|
||||
{ label: 'True', value: true },
|
||||
@@ -85,4 +86,8 @@
|
||||
]}
|
||||
bind:value={data.default} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+7
-2
@@ -46,6 +46,7 @@
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnDatetime>;
|
||||
|
||||
let savedDefault = data.default;
|
||||
@@ -77,7 +78,11 @@
|
||||
id="default"
|
||||
label="Default value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={!data.required && !data.array} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+7
-2
@@ -42,6 +42,7 @@
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnEmail>;
|
||||
|
||||
let savedDefault = data.default;
|
||||
@@ -74,7 +75,11 @@
|
||||
label="Default value"
|
||||
placeholder="Enter value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={!data.required && !data.array} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+8
-2
@@ -46,6 +46,7 @@
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnEnum>;
|
||||
|
||||
let savedDefault = data.default;
|
||||
@@ -67,6 +68,7 @@
|
||||
array: false,
|
||||
...data
|
||||
});
|
||||
|
||||
$: listen(data);
|
||||
|
||||
$: handleDefaultState($required || $array);
|
||||
@@ -103,9 +105,13 @@
|
||||
<InputSelect
|
||||
id="default"
|
||||
label="Default value"
|
||||
disabled={data.array || data.required}
|
||||
disabled={data.array || data.required || disabled}
|
||||
placeholder="Select a value"
|
||||
{options}
|
||||
bind:value={data.default} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+11
-2
@@ -47,6 +47,7 @@
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnFloat> = {
|
||||
required: false,
|
||||
min: 0,
|
||||
@@ -86,15 +87,19 @@
|
||||
placeholder="Enter size"
|
||||
bind:value={data.min}
|
||||
step={0.1}
|
||||
{disabled}
|
||||
required={editing} />
|
||||
|
||||
<InputNumber
|
||||
id="max"
|
||||
label="Max"
|
||||
placeholder="Enter size"
|
||||
bind:value={data.max}
|
||||
step={0.1}
|
||||
{disabled}
|
||||
required={editing} />
|
||||
</Layout.Stack>
|
||||
|
||||
<InputNumber
|
||||
id="default"
|
||||
label="Default value"
|
||||
@@ -102,8 +107,12 @@
|
||||
min={data.min}
|
||||
max={data.max}
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={!data.required && !data.array}
|
||||
step={0.1} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+12
-4
@@ -47,7 +47,7 @@
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let editing = false;
|
||||
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnInteger> = {
|
||||
required: false,
|
||||
min: 0,
|
||||
@@ -84,16 +84,20 @@
|
||||
<InputNumber
|
||||
id="min"
|
||||
label="Min"
|
||||
{disabled}
|
||||
placeholder="Enter size"
|
||||
bind:value={data.min}
|
||||
required={editing} />
|
||||
|
||||
<InputNumber
|
||||
id="max"
|
||||
label="Max"
|
||||
{disabled}
|
||||
placeholder="Enter size"
|
||||
bind:value={data.max}
|
||||
required={editing} />
|
||||
</Layout.Stack>
|
||||
|
||||
<InputNumber
|
||||
id="default"
|
||||
label="Default value"
|
||||
@@ -101,7 +105,11 @@
|
||||
min={data.min}
|
||||
max={data.max}
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
nullable={!data.required && !data.array} />
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={(!data.required && !data.array) || disabled} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+7
-2
@@ -41,6 +41,7 @@
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnIp>;
|
||||
|
||||
let savedDefault = data.default;
|
||||
@@ -73,7 +74,11 @@
|
||||
label="Default value"
|
||||
placeholder="Enter value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={!data.required && !data.array} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+12
-3
@@ -42,11 +42,16 @@
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
data?: Partial<Models.ColumnLine>;
|
||||
editing?: boolean;
|
||||
disabled?: boolean;
|
||||
data?: Partial<Models.ColumnLine>;
|
||||
}
|
||||
|
||||
let { data = { required: false, default: null }, editing = false }: Props = $props();
|
||||
let {
|
||||
data = { required: false, default: null },
|
||||
editing = false,
|
||||
disabled = false
|
||||
}: Props = $props();
|
||||
|
||||
let savedDefault = $state(data.default);
|
||||
let defaultChecked = $state(!!data.default);
|
||||
@@ -106,6 +111,7 @@
|
||||
size="s"
|
||||
id="required"
|
||||
label="Required"
|
||||
{disabled}
|
||||
bind:checked={$required}
|
||||
on:change={(e) => {
|
||||
if (e.detail) defaultChecked = false;
|
||||
@@ -116,6 +122,7 @@
|
||||
size="s"
|
||||
id="default"
|
||||
label="Default value"
|
||||
{disabled}
|
||||
bind:checked={defaultChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
@@ -134,11 +141,13 @@
|
||||
<Typography.Caption variant="400">Optional</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<InputLine
|
||||
{disabled}
|
||||
values={defaultChecked ? data.default : null}
|
||||
onAddPoint={() => pushCoordinate()}
|
||||
onDeletePoint={deleteCoordinate}
|
||||
onChangePoint={(pointIndex: number, coordIndex: number, newValue: number) => {
|
||||
onChangePoint={(pointIndex, coordIndex, newValue) => {
|
||||
if (data.default) {
|
||||
data.default[pointIndex][coordIndex] = newValue;
|
||||
data.default = [...data.default];
|
||||
|
||||
+13
-2
@@ -42,11 +42,19 @@
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
data?: Partial<Models.ColumnPoint>;
|
||||
editing?: boolean;
|
||||
disabled?: boolean;
|
||||
data?: Partial<Models.ColumnPoint>;
|
||||
}
|
||||
|
||||
let { data = { required: false, default: null }, editing }: Props = $props();
|
||||
let {
|
||||
data = {
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
editing = false,
|
||||
disabled = false
|
||||
}: Props = $props();
|
||||
|
||||
let savedDefault = $state(data.default);
|
||||
let defaultChecked = $state(!!data.default);
|
||||
@@ -96,6 +104,7 @@
|
||||
size="s"
|
||||
id="required"
|
||||
label="Required"
|
||||
{disabled}
|
||||
bind:checked={$required}
|
||||
on:change={(e) => {
|
||||
if (e.detail) defaultChecked = false;
|
||||
@@ -106,6 +115,7 @@
|
||||
size="s"
|
||||
id="default"
|
||||
label="Default value"
|
||||
{disabled}
|
||||
bind:checked={defaultChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
@@ -126,6 +136,7 @@
|
||||
{/if}
|
||||
|
||||
<InputPoint
|
||||
{disabled}
|
||||
values={defaultChecked ? data.default : null}
|
||||
onChangePoint={(index, newValue) => {
|
||||
if (data.default) {
|
||||
|
||||
+13
-2
@@ -42,11 +42,19 @@
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
data?: Partial<Models.ColumnPolygon>;
|
||||
editing?: boolean;
|
||||
disabled?: boolean;
|
||||
data?: Partial<Models.ColumnPolygon>;
|
||||
}
|
||||
|
||||
let { data = { required: false, default: null }, editing = false }: Props = $props();
|
||||
let {
|
||||
data = {
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
editing = false,
|
||||
disabled = false
|
||||
}: Props = $props();
|
||||
|
||||
let savedDefault = $state(data.default);
|
||||
let defaultChecked = $state(!!data.default);
|
||||
@@ -117,6 +125,7 @@
|
||||
size="s"
|
||||
id="required"
|
||||
label="Required"
|
||||
{disabled}
|
||||
bind:checked={$required}
|
||||
on:change={(e) => {
|
||||
if (e.detail) defaultChecked = false;
|
||||
@@ -128,6 +137,7 @@
|
||||
size="s"
|
||||
id="default"
|
||||
label="Default value"
|
||||
{disabled}
|
||||
bind:checked={defaultChecked}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
@@ -148,6 +158,7 @@
|
||||
{/if}
|
||||
|
||||
<InputPolygon
|
||||
{disabled}
|
||||
values={defaultChecked ? data.default : null}
|
||||
onAddLine={pushLine}
|
||||
onAddPoint={pushCoordinate}
|
||||
|
||||
+9
-5
@@ -69,6 +69,7 @@
|
||||
|
||||
// Props
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Models.ColumnRelationship;
|
||||
|
||||
// Constants
|
||||
@@ -158,7 +159,7 @@
|
||||
bind:group={way}
|
||||
name="one"
|
||||
value="one"
|
||||
disabled={editing}
|
||||
disabled={editing || disabled}
|
||||
icon={IconArrowSmRight}>
|
||||
One Relation column within this table
|
||||
</Card.Selector>
|
||||
@@ -167,7 +168,7 @@
|
||||
bind:group={way}
|
||||
name="two"
|
||||
value="two"
|
||||
disabled={editing}
|
||||
disabled={editing || disabled}
|
||||
icon={IconSwitchHorizontal}>
|
||||
One Relation column within this table and another within the related table
|
||||
</Card.Selector>
|
||||
@@ -180,7 +181,7 @@
|
||||
placeholder="Select a table"
|
||||
bind:value={data.relatedTable}
|
||||
on:change={updateKeyName}
|
||||
disabled={editing}
|
||||
disabled={editing || disabled}
|
||||
options={tables?.map((n) => ({ value: n.$id, label: `${n.name} (${n.$id})` })) ?? []} />
|
||||
|
||||
{#if data?.relatedTable}
|
||||
@@ -190,7 +191,8 @@
|
||||
placeholder="Enter key"
|
||||
bind:value={data.key}
|
||||
helper="Allowed characters: a-z, A-Z, 0-9, -, ."
|
||||
required />
|
||||
required
|
||||
{disabled} />
|
||||
|
||||
{#if way === 'two'}
|
||||
<InputText
|
||||
@@ -199,6 +201,7 @@
|
||||
placeholder="Enter key"
|
||||
bind:value={data.twoWayKey}
|
||||
required
|
||||
{disabled}
|
||||
helper="Allowed characters: a-z, A-Z, 0-9, -, . Once created, column key cannot be
|
||||
adjusted to maintain data integrity."
|
||||
readonly={editing} />
|
||||
@@ -211,7 +214,7 @@
|
||||
required
|
||||
placeholder="Select a relation"
|
||||
options={relationshipType}
|
||||
disabled={editing} />
|
||||
disabled={editing || disabled} />
|
||||
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Box>
|
||||
@@ -251,6 +254,7 @@
|
||||
label="On deleting a row"
|
||||
bind:value={data.onDelete}
|
||||
required
|
||||
{disabled}
|
||||
placeholder="Select a deletion method"
|
||||
options={deleteOptions} />
|
||||
{/if}
|
||||
|
||||
+8
-6
@@ -4,22 +4,24 @@
|
||||
let {
|
||||
required = $bindable(false),
|
||||
array = $bindable(false),
|
||||
editing = false
|
||||
editing = false,
|
||||
disabled = false
|
||||
}: {
|
||||
required: boolean;
|
||||
array: boolean;
|
||||
editing: boolean;
|
||||
editing?: boolean;
|
||||
disabled?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip disabled={!array} maxWidth="275px" placement="bottom-start">
|
||||
<Tooltip disabled={!array || disabled} maxWidth="275px" placement="bottom-start">
|
||||
<div style:width="fit-content">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="required"
|
||||
label="Required"
|
||||
bind:checked={required}
|
||||
disabled={array}
|
||||
disabled={array || disabled}
|
||||
description="Indicate whether this column is required." />
|
||||
</div>
|
||||
|
||||
@@ -28,14 +30,14 @@
|
||||
</svelte:fragment>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip disabled={!(required || editing)} maxWidth="275px" placement="bottom-start">
|
||||
<Tooltip disabled={!(required || editing) || disabled} maxWidth="275px" placement="bottom-start">
|
||||
<div style:width="fit-content">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="array"
|
||||
label="Array"
|
||||
bind:checked={array}
|
||||
disabled={required || editing}
|
||||
disabled={required || editing || disabled}
|
||||
description="Indicate whether this column is an array. Defaults to an empty array." />
|
||||
</div>
|
||||
|
||||
|
||||
+18
-7
@@ -56,6 +56,8 @@
|
||||
};
|
||||
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let autoIncreaseSize = false;
|
||||
|
||||
let savedDefault = data.default;
|
||||
|
||||
@@ -83,12 +85,17 @@
|
||||
|
||||
// Check plan on cloud, always allow on self-hosted
|
||||
$: supportsStringEncryption = isCloud ? $currentPlan?.databasesAllowEncrypt : true;
|
||||
|
||||
$: if (autoIncreaseSize && data.encrypt && data.size < 150) {
|
||||
data.size = 150;
|
||||
}
|
||||
</script>
|
||||
|
||||
<InputNumber
|
||||
id="size"
|
||||
label="Size"
|
||||
required
|
||||
{disabled}
|
||||
placeholder="Enter size"
|
||||
bind:value={data.size}
|
||||
min={supportsStringEncryption && data.encrypt ? 150 : undefined}
|
||||
@@ -103,30 +110,34 @@
|
||||
placeholder="Enter string"
|
||||
maxlength={data.size}
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={!data.required && !data.array} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
<Layout.Stack gap="xs" direction="column">
|
||||
<div
|
||||
class="popover-holder"
|
||||
class:cursor-not-allowed={editing}
|
||||
class:disabled-checkbox={!supportsStringEncryption || editing}>
|
||||
class:cursor-not-allowed={editing || disabled}
|
||||
class:disabled-checkbox={!supportsStringEncryption || editing || disabled}>
|
||||
<Layout.Stack inline gap="s" alignItems="flex-start" direction="row">
|
||||
<Popover let:toggle placement="bottom-start">
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
id="encrypt"
|
||||
bind:checked={data.encrypt}
|
||||
disabled={!supportsStringEncryption || editing} />
|
||||
disabled={!supportsStringEncryption || editing || disabled} />
|
||||
|
||||
<Layout.Stack gap="xxs" direction="column">
|
||||
<button
|
||||
type="button"
|
||||
disabled={editing}
|
||||
disabled={editing || disabled}
|
||||
class:cursor-pointer={!editing}
|
||||
class:cursor-not-allowed={editing}
|
||||
class:cursor-not-allowed={editing || disabled}
|
||||
on:click={(e) => {
|
||||
if (!supportsStringEncryption) {
|
||||
toggle(e);
|
||||
|
||||
+8
-3
@@ -41,8 +41,9 @@
|
||||
import { createConservative } from '$lib/helpers/stores';
|
||||
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
|
||||
|
||||
export let data: Partial<Models.ColumnUrl>;
|
||||
export let editing = false;
|
||||
export let disabled = false;
|
||||
export let data: Partial<Models.ColumnUrl>;
|
||||
|
||||
let savedDefault = data.default;
|
||||
|
||||
@@ -74,7 +75,11 @@
|
||||
label="Default value"
|
||||
placeholder="Enter value"
|
||||
bind:value={data.default}
|
||||
disabled={data.required || data.array}
|
||||
disabled={data.required || data.array || disabled}
|
||||
nullable={!data.required && !data.array} />
|
||||
|
||||
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
|
||||
<RequiredArrayCheckboxes
|
||||
{editing}
|
||||
{disabled}
|
||||
bind:array={data.array}
|
||||
bind:required={data.required} />
|
||||
|
||||
+44
-2
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { type Columns, type ColumnDirection } from './store';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { Alert, Layout, Link } from '@appwrite.io/pink-svelte';
|
||||
import { InputSelect, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
@@ -12,6 +11,12 @@
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { showColumnsSuggestionsModal } from '../(suggestions)/store';
|
||||
import IconAINotification from '../(suggestions)/icon/aiNotification.svelte';
|
||||
import { type Columns, type ColumnDirection, showCreateColumnSheet } from './store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let {
|
||||
direction = null,
|
||||
column = null,
|
||||
@@ -35,6 +40,8 @@
|
||||
const tableId = page.params.table;
|
||||
const databaseId = page.params.database;
|
||||
|
||||
let showSuggestionsAlert = $state(true);
|
||||
|
||||
let key: string = $state(column?.key ?? null);
|
||||
let data: Partial<Columns> = $state({
|
||||
required: column?.required ?? false,
|
||||
@@ -180,6 +187,22 @@
|
||||
</script>
|
||||
|
||||
<Layout.Stack gap="xl">
|
||||
{#if isCloud && showSuggestionsAlert}
|
||||
<div class="custom-inline-alert" transition:slide>
|
||||
<Alert.Inline dismissible on:dismiss={() => (showSuggestionsAlert = false)}>
|
||||
<svelte:fragment slot="icon">
|
||||
<IconAINotification />
|
||||
</svelte:fragment>
|
||||
|
||||
Need help? Let AI <Link.Button
|
||||
on:click={() => {
|
||||
$showCreateColumnSheet.show = false;
|
||||
$showColumnsSuggestionsModal = true;
|
||||
}}>suggest columns</Link.Button> based on your data
|
||||
</Alert.Inline>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Layout.Stack direction="row">
|
||||
<InputText
|
||||
id="key"
|
||||
@@ -209,3 +232,22 @@
|
||||
<ColumnComponent bind:data onclose={() => ($option = null)} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-inline-alert {
|
||||
& :global(article) {
|
||||
border-radius: var(--border-radius-medium);
|
||||
padding: var(--space-4, 8px);
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
border: var(--border-width-s) solid var(--border-neutral);
|
||||
}
|
||||
|
||||
& :global(div:first-child > :nth-child(2)) {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
& :global(.ai-icon-holder.notification) {
|
||||
height: 36px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { Fieldset, Layout, Icon, Divider, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { Button, InputSelect, InputCheckbox, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import { table } from '../store';
|
||||
import { queries, type TagValue } from '$lib/components/filters/store';
|
||||
import { TagList } from '$lib/components/filters';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { toLocalDateTimeISO } from '$lib/helpers/date';
|
||||
import { writable } from 'svelte/store';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
|
||||
let showExitModal = $state(false);
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
let localQueries = $state<Map<TagValue, string>>(new Map());
|
||||
const localTags = $derived(Array.from(localQueries.keys()));
|
||||
|
||||
const timestamp = toLocalDateTimeISO(Date.now())
|
||||
.replace(/[:.]/g, '-')
|
||||
.split('T')
|
||||
.join('_')
|
||||
.slice(0, -4);
|
||||
const filename = `${$table.name}_${timestamp}.csv`;
|
||||
|
||||
let selectedColumns = $state<Record<string, boolean>>({});
|
||||
let showAllColumns = $state(false);
|
||||
|
||||
type DelimiterOption = 'Comma' | 'Semicolon' | 'Tab' | 'Pipe';
|
||||
const delimiterMap: Record<DelimiterOption, string> = {
|
||||
Comma: ',',
|
||||
Semicolon: ';',
|
||||
Tab: '\t',
|
||||
Pipe: '|'
|
||||
};
|
||||
|
||||
let delimiter = $state<DelimiterOption>('Comma');
|
||||
let includeHeader = $state(true);
|
||||
let exportWithFilters = $state(false);
|
||||
|
||||
const columnLimit = $derived($isSmallViewport ? 6 : 9);
|
||||
const visibleColumns = $derived(
|
||||
showAllColumns ? $table.columns : $table.columns.slice(0, columnLimit)
|
||||
);
|
||||
const hasMoreColumns = $derived($table.columns.length > columnLimit);
|
||||
const selectedColumnCount = $derived(Object.values(selectedColumns).filter(Boolean).length);
|
||||
|
||||
const tableUrl = $derived.by(() => {
|
||||
const queryParam = page.url.searchParams.get('query');
|
||||
const url = resolve(
|
||||
'/(console)/project-[region]-[project]/databases/database-[database]/table-[table]',
|
||||
{
|
||||
region: page.params.region,
|
||||
project: page.params.project,
|
||||
database: page.params.database,
|
||||
table: page.params.table
|
||||
}
|
||||
);
|
||||
return queryParam ? `${url}?query=${queryParam}` : url;
|
||||
});
|
||||
|
||||
function removeLocalFilter(tag: TagValue) {
|
||||
localQueries.delete(tag);
|
||||
localQueries = new Map(localQueries);
|
||||
}
|
||||
|
||||
function initializeColumns() {
|
||||
selectedColumns = Object.fromEntries($table.columns.map((col) => [col.key, true]));
|
||||
}
|
||||
|
||||
function selectAllColumns() {
|
||||
selectedColumns = Object.fromEntries($table.columns.map((col) => [col.key, true]));
|
||||
}
|
||||
|
||||
function deselectAllColumns() {
|
||||
selectedColumns = Object.fromEntries($table.columns.map((col) => [col.key, false]));
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const selectedCols = Object.entries(selectedColumns)
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([key]) => key);
|
||||
|
||||
if (selectedCols.length === 0) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Please select at least one column to export'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.migrations.createCSVExport({
|
||||
resourceId: `${page.params.database}:${page.params.table}`,
|
||||
filename: filename,
|
||||
columns: selectedCols,
|
||||
queries: exportWithFilters ? Array.from(localQueries.values()) : [],
|
||||
delimiter: delimiterMap[delimiter],
|
||||
header: includeHeader,
|
||||
notify: true
|
||||
});
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'CSV export has started'
|
||||
});
|
||||
|
||||
trackEvent(Submit.DatabaseExportCsv);
|
||||
|
||||
await goto(tableUrl);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
|
||||
trackError(error, Submit.DatabaseExportCsv);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initializeColumns();
|
||||
localQueries = new Map($queries);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard title="Export CSV" columnSize="s" href={tableUrl} bind:showExitModal confirmExit column>
|
||||
<Form bind:this={formComponent} bind:isSubmitting onSubmit={handleExport}>
|
||||
<Layout.Stack gap="xxl">
|
||||
<Fieldset legend="Columns">
|
||||
<Layout.Stack gap="l">
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center">
|
||||
<Button compact on:click={selectAllColumns}>Select all</Button>
|
||||
<span style:height="20px">
|
||||
<Divider vertical />
|
||||
</span>
|
||||
<Button compact on:click={deselectAllColumns}>Deselect all</Button>
|
||||
</Layout.Stack>
|
||||
|
||||
<Layout.Grid columns={3} columnsS={1} gap="l">
|
||||
{#each visibleColumns as column (column.key)}
|
||||
<div style="min-width: 0;">
|
||||
<InputCheckbox
|
||||
id={`column-${column.key}`}
|
||||
label={column.key}
|
||||
bind:checked={selectedColumns[column.key]}
|
||||
truncate />
|
||||
</div>
|
||||
{/each}
|
||||
</Layout.Grid>
|
||||
|
||||
{#if hasMoreColumns}
|
||||
<div style:margin-bottom="-0.5rem">
|
||||
<Button compact on:click={() => (showAllColumns = !showAllColumns)}>
|
||||
{showAllColumns ? 'Show less' : 'Show more'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Export options">
|
||||
<Layout.Stack gap="l">
|
||||
<InputSelect
|
||||
id="delimiter"
|
||||
label="Delimiter"
|
||||
bind:value={delimiter}
|
||||
options={[
|
||||
{ value: 'Comma', label: 'Comma' },
|
||||
{ value: 'Semicolon', label: 'Semicolon' },
|
||||
{ value: 'Tab', label: 'Tab' },
|
||||
{ value: 'Pipe', label: 'Pipe' }
|
||||
]}>
|
||||
<Layout.Stack direction="row" gap="none" alignItems="center" slot="info">
|
||||
<Tooltip>
|
||||
<Icon size="s" icon={IconInfo} />
|
||||
<span slot="tooltip">
|
||||
Define how to separate values in the exported file.
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Layout.Stack>
|
||||
</InputSelect>
|
||||
|
||||
<InputCheckbox
|
||||
id="includeHeader"
|
||||
label="Include header row"
|
||||
description="Column names will be added as the first row in the CSV"
|
||||
bind:checked={includeHeader} />
|
||||
|
||||
<Layout.Stack gap="m">
|
||||
<div class:disabled-checkbox={localTags.length === 0}>
|
||||
<InputCheckbox
|
||||
id="exportWithFilters"
|
||||
label="Export with filters"
|
||||
description="Export rows that match the current table filters"
|
||||
disabled={localTags.length === 0}
|
||||
bind:checked={exportWithFilters} />
|
||||
</div>
|
||||
|
||||
{#if localTags.length > 0}
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
gap="xs"
|
||||
alignItems="center"
|
||||
style="padding-left: 1.75rem;"
|
||||
wrap="wrap">
|
||||
<TagList
|
||||
tags={localTags}
|
||||
on:remove={(e) => {
|
||||
removeLocalFilter(e.detail);
|
||||
}} />
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack justifyContent="flex-end" direction="row">
|
||||
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting || selectedColumnCount === 0}>
|
||||
Export
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Wizard>
|
||||
|
||||
<style>
|
||||
.disabled-checkbox :global(*) {
|
||||
cursor: unset;
|
||||
}
|
||||
</style>
|
||||
+101
-23
@@ -23,6 +23,7 @@
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconBookOpen,
|
||||
IconDotsHorizontal,
|
||||
IconEye,
|
||||
IconPlus,
|
||||
@@ -37,6 +38,10 @@
|
||||
import { showCreateColumnSheet } from '../store';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { page } from '$app/state';
|
||||
import { showIndexesSuggestions, showColumnsSuggestionsModal } from '../../(suggestions)';
|
||||
import IconAI from '../../(suggestions)/icon/aiForButton.svelte';
|
||||
import EmptySheetCards from '../layout/emptySheetCards.svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { realtime } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
@@ -66,14 +71,14 @@
|
||||
const spreadsheetColumns = $derived([
|
||||
{
|
||||
id: 'key',
|
||||
width: getColumnWidth('key', $isSmallViewport ? 250 : 200),
|
||||
minimumWidth: $isSmallViewport ? 250 : 200,
|
||||
width: getColumnWidth('key', 250),
|
||||
minimumWidth: 250,
|
||||
resizable: true
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
width: getColumnWidth('type', 120),
|
||||
minimumWidth: 120,
|
||||
width: getColumnWidth('type', 200),
|
||||
minimumWidth: 200,
|
||||
resizable: true
|
||||
},
|
||||
{
|
||||
@@ -296,27 +301,100 @@
|
||||
</Spreadsheet.Root>
|
||||
</SpreadsheetContainer>
|
||||
{:else}
|
||||
<EmptySheet
|
||||
mode="indexes"
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => (showCreateIndex = true),
|
||||
disabled: !$table?.columns?.length
|
||||
}
|
||||
}} />
|
||||
<EmptySheet mode="indexes" showActions={$canWriteTables}>
|
||||
{#snippet subtitle()}
|
||||
{#if isCloud}
|
||||
<Typography.Text align="center">
|
||||
Need a hand? Learn more in the
|
||||
<Link.Anchor
|
||||
target="_blank"
|
||||
href="https://appwrite.io/docs/products/databases/tables#indexes">
|
||||
docs.
|
||||
</Link.Anchor>
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet actions()}
|
||||
{#if isCloud}
|
||||
<EmptySheetCards
|
||||
icon={IconAI}
|
||||
title="Suggest indexes"
|
||||
disabled={!$table?.columns?.length}
|
||||
subtitle="Use AI to generate indexes"
|
||||
onClick={() => {
|
||||
showIndexesSuggestions.update(() => true);
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
<EmptySheetCards
|
||||
icon={IconPlus}
|
||||
title="Create index"
|
||||
disabled={!$table?.columns?.length}
|
||||
subtitle="Create indexes manually"
|
||||
onClick={() => {
|
||||
showCreateIndex = true;
|
||||
}} />
|
||||
|
||||
{#if !isCloud}
|
||||
<EmptySheetCards
|
||||
icon={IconBookOpen}
|
||||
title="Documentation"
|
||||
subtitle="Read the Appwrite docs"
|
||||
href="https://appwrite.io/docs/products/databases/tables#indexes" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EmptySheet>
|
||||
{/if}
|
||||
{:else}
|
||||
<EmptySheet
|
||||
mode="indexes"
|
||||
title="You have no columns yet"
|
||||
actions={{
|
||||
primary: {
|
||||
text: 'Create columns',
|
||||
onClick: async () => {
|
||||
$showCreateColumnSheet.show = true;
|
||||
}
|
||||
}
|
||||
}} />
|
||||
<EmptySheet mode="indexes" title="You have no columns yet" showActions={$canWriteTables}>
|
||||
{#snippet subtitle()}
|
||||
{#if isCloud}
|
||||
<Typography.Text align="center">
|
||||
Need a hand? Learn more in the
|
||||
<Link.Anchor
|
||||
target="_blank"
|
||||
href="https://appwrite.io/docs/products/databases/tables#columns">
|
||||
docs.
|
||||
</Link.Anchor>
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet actions()}
|
||||
{#if isCloud}
|
||||
<EmptySheetCards
|
||||
icon={IconAI}
|
||||
title="Suggest columns"
|
||||
subtitle="Use AI to generate columns"
|
||||
onClick={() => {
|
||||
$showColumnsSuggestionsModal = true;
|
||||
}} />
|
||||
|
||||
<EmptySheetCards
|
||||
icon={IconPlus}
|
||||
title="Create column"
|
||||
subtitle="Create columns manually"
|
||||
onClick={() => {
|
||||
$showCreateColumnSheet.show = true;
|
||||
}} />
|
||||
{:else}
|
||||
<EmptySheetCards
|
||||
icon={IconPlus}
|
||||
title="Create column"
|
||||
subtitle="Create columns manually"
|
||||
onClick={() => {
|
||||
$showCreateColumnSheet.show = true;
|
||||
}} />
|
||||
|
||||
<EmptySheetCards
|
||||
icon={IconBookOpen}
|
||||
title="Documentation"
|
||||
subtitle="Read the Appwrite docs"
|
||||
href="https://appwrite.io/docs/products/databases/tables#columns" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EmptySheet>
|
||||
{/if}
|
||||
|
||||
{#if selectedIndexes.length > 0}
|
||||
|
||||
+2
-1
@@ -71,7 +71,8 @@
|
||||
// spatial type selected -> reset column list to single empty column
|
||||
// and the column already is not spatial type
|
||||
$effect(() => {
|
||||
if (selectedType === IndexType.Spatial && !columnList.at(0).value) {
|
||||
const firstColumn = $table.columns.find((col) => col.key === columnList.at(0)?.value);
|
||||
if (selectedType === IndexType.Spatial && firstColumn && !isSpatialType(firstColumn)) {
|
||||
columnList = [{ value: '', order: null, length: null }];
|
||||
}
|
||||
});
|
||||
|
||||
+261
-159
@@ -19,43 +19,57 @@
|
||||
expandTabs
|
||||
} from '../store';
|
||||
import SpreadsheetContainer from './spreadsheet.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { columnOptions } from '../columns/store';
|
||||
|
||||
type Mode = 'rows' | 'rows-filtered' | 'indexes';
|
||||
|
||||
interface Action {
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const {
|
||||
mode,
|
||||
showActions = true,
|
||||
customColumns = [],
|
||||
title,
|
||||
actions
|
||||
subtitle,
|
||||
actions,
|
||||
showActions
|
||||
} = $props<{
|
||||
mode: Mode;
|
||||
showActions?: boolean;
|
||||
customColumns?: Column[];
|
||||
title?: string;
|
||||
actions?: {
|
||||
primary?: Action;
|
||||
random?: Action;
|
||||
};
|
||||
subtitle?: Snippet;
|
||||
actions?: Snippet;
|
||||
showActions?: boolean;
|
||||
}>();
|
||||
|
||||
let spreadsheetContainer: HTMLElement;
|
||||
let headerElement: HTMLElement | null = null;
|
||||
|
||||
let resizeObserver: ResizeObserver;
|
||||
let overlayOffsetHandler: ResizeObserver;
|
||||
|
||||
let overlayLeftOffset = $state('0px');
|
||||
let overlayTopOffset = $state('auto');
|
||||
let dynamicOverlayHeight = $state('60.5vh');
|
||||
|
||||
const baseColProps = { draggable: false, resizable: false };
|
||||
|
||||
const updateOverlayLeftOffset = () => {
|
||||
if (spreadsheetContainer) {
|
||||
const containerRect = spreadsheetContainer.getBoundingClientRect();
|
||||
overlayLeftOffset = `${containerRect.left}px`;
|
||||
}
|
||||
|
||||
// calculate vertical top position
|
||||
if (!headerElement || !headerElement.isConnected) {
|
||||
headerElement = spreadsheetContainer?.querySelector('[role="rowheader"]');
|
||||
}
|
||||
|
||||
if (headerElement) {
|
||||
const headerRect = headerElement.getBoundingClientRect();
|
||||
overlayTopOffset = `${headerRect.bottom}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const updateOverlayHeight = () => {
|
||||
if (!spreadsheetContainer) return;
|
||||
|
||||
@@ -82,6 +96,9 @@
|
||||
if (spreadsheetContainer) {
|
||||
resizeObserver = new ResizeObserver(debouncedUpdateOverlayHeight);
|
||||
resizeObserver.observe(spreadsheetContainer);
|
||||
|
||||
overlayOffsetHandler = new ResizeObserver(updateOverlayLeftOffset);
|
||||
overlayOffsetHandler.observe(spreadsheetContainer);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,74 +106,141 @@
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
if (overlayOffsetHandler) {
|
||||
overlayOffsetHandler.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
const getCustomColumns = (): Column[] =>
|
||||
customColumns.map((col: Column) => ({
|
||||
...col,
|
||||
width: 180,
|
||||
hide: false,
|
||||
icon: columnOptions.find((colOpt) => colOpt.type === col?.type)?.icon,
|
||||
...baseColProps
|
||||
}));
|
||||
|
||||
const getRowColumns = (): Column[] => [
|
||||
{
|
||||
id: '$id',
|
||||
title: '$id',
|
||||
type: 'string',
|
||||
width: 180,
|
||||
icon: IconFingerPrint,
|
||||
...baseColProps
|
||||
},
|
||||
...getCustomColumns(),
|
||||
{
|
||||
id: '$createdAt',
|
||||
title: '$createdAt',
|
||||
type: 'datetime',
|
||||
width: 180,
|
||||
icon: IconCalendar,
|
||||
...baseColProps
|
||||
},
|
||||
{
|
||||
id: '$updatedAt',
|
||||
title: '$updatedAt',
|
||||
type: 'datetime',
|
||||
width: 180,
|
||||
icon: IconCalendar,
|
||||
...baseColProps
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
title: '',
|
||||
type: 'string',
|
||||
icon: IconPlus,
|
||||
width: customColumns.length ? 555 : 832,
|
||||
...baseColProps
|
||||
},
|
||||
{
|
||||
id: 'empty',
|
||||
title: '',
|
||||
type: 'string',
|
||||
...baseColProps
|
||||
}
|
||||
];
|
||||
const getRowColumns = (): Column[] => {
|
||||
const minColumnWidth = 180;
|
||||
const fixedWidths = { id: 180, actions: 40 };
|
||||
const hasCustomColumns = customColumns.length > 0;
|
||||
|
||||
const getIndexesColumns = (): Column[] =>
|
||||
[
|
||||
const customColumnsData = getCustomColumns();
|
||||
|
||||
// Calculate column widths based on whether we have custom columns
|
||||
let columnWidths = {
|
||||
id: fixedWidths.id,
|
||||
createdAt: fixedWidths.id,
|
||||
updatedAt: fixedWidths.id,
|
||||
custom: minColumnWidth,
|
||||
actions: hasCustomColumns ? fixedWidths.actions : 1387
|
||||
};
|
||||
|
||||
if (hasCustomColumns) {
|
||||
const equalWidthColumns = [
|
||||
...customColumnsData,
|
||||
{ id: '$createdAt' },
|
||||
{ id: '$updatedAt' }
|
||||
];
|
||||
|
||||
const totalBaseWidth =
|
||||
fixedWidths.id + fixedWidths.actions + equalWidthColumns.length * minColumnWidth;
|
||||
|
||||
const viewportWidth =
|
||||
spreadsheetContainer?.clientWidth ||
|
||||
(typeof window !== 'undefined' ? window.innerWidth : totalBaseWidth);
|
||||
|
||||
const excessSpace = Math.max(0, viewportWidth - totalBaseWidth);
|
||||
const extraPerColumn =
|
||||
equalWidthColumns.length > 0 ? excessSpace / equalWidthColumns.length : 0;
|
||||
const distributedWidth = minColumnWidth + extraPerColumn;
|
||||
|
||||
columnWidths.createdAt = distributedWidth;
|
||||
columnWidths.updatedAt = distributedWidth;
|
||||
columnWidths.custom = distributedWidth;
|
||||
}
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
id: '$id',
|
||||
title: '$id',
|
||||
type: 'string',
|
||||
width: columnWidths.id,
|
||||
icon: IconFingerPrint,
|
||||
...baseColProps
|
||||
}
|
||||
];
|
||||
|
||||
if (hasCustomColumns) {
|
||||
columns.push(
|
||||
...customColumnsData.map((col) => ({
|
||||
...col,
|
||||
width: columnWidths.custom
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
id: '$createdAt',
|
||||
title: '$createdAt',
|
||||
type: 'datetime',
|
||||
width: columnWidths.createdAt,
|
||||
icon: IconCalendar,
|
||||
...baseColProps
|
||||
},
|
||||
{
|
||||
id: '$updatedAt',
|
||||
title: '$updatedAt',
|
||||
type: 'datetime',
|
||||
width: columnWidths.updatedAt,
|
||||
icon: IconCalendar,
|
||||
...baseColProps
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
title: '',
|
||||
type: 'string',
|
||||
icon: IconPlus,
|
||||
isAction: hasCustomColumns,
|
||||
width: columnWidths.actions,
|
||||
...baseColProps
|
||||
}
|
||||
);
|
||||
|
||||
if (!hasCustomColumns) {
|
||||
columns.push({
|
||||
id: 'empty',
|
||||
title: '',
|
||||
type: 'string',
|
||||
...baseColProps
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
const getIndexesColumns = (): Column[] => {
|
||||
const columns = [
|
||||
{ id: 'key', title: 'Key', icon: null, isPrimary: false },
|
||||
{ id: 'type', title: 'Type', icon: null, isPrimary: false },
|
||||
{ id: 'columns', title: 'Columns', icon: null, isPrimary: false },
|
||||
{
|
||||
{ id: 'columns', title: 'Columns', icon: null, isPrimary: false }
|
||||
] as Column[];
|
||||
|
||||
if (!$isSmallViewport) {
|
||||
columns.push({
|
||||
id: 'empty',
|
||||
title: '',
|
||||
width: 40,
|
||||
isAction: true,
|
||||
isPrimary: false
|
||||
}
|
||||
] as Column[];
|
||||
} as Column);
|
||||
}
|
||||
|
||||
const spreadsheetColumns = $derived(mode === 'rows' ? getRowColumns() : getIndexesColumns());
|
||||
return columns;
|
||||
};
|
||||
|
||||
const spreadsheetColumns = $derived(mode === 'indexes' ? getIndexesColumns() : getRowColumns());
|
||||
|
||||
const emptyCells = $derived(
|
||||
($isSmallViewport ? 14 : $isTabletViewport ? 17 : 24) + (!$expandTabs ? 2 : 0)
|
||||
@@ -164,9 +248,11 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="databases-spreadsheet spreadsheet-container-outer"
|
||||
data-mode={mode}
|
||||
bind:this={spreadsheetContainer}>
|
||||
bind:this={spreadsheetContainer}
|
||||
class:custom-columns={customColumns.length > 0}
|
||||
class:no-custom-columns={customColumns.length <= 0}
|
||||
class="databases-spreadsheet spreadsheet-container-outer">
|
||||
<SpreadsheetContainer>
|
||||
<Spreadsheet.Root
|
||||
{emptyCells}
|
||||
@@ -179,20 +265,23 @@
|
||||
}}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
{#each spreadsheetColumns as column (column.id)}
|
||||
{@const columnActionsById = column.id === 'actions'}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
style:cursor={columnActionsById ? 'pointer' : null}
|
||||
onclick={() => {
|
||||
if (columnActionsById && mode === 'rows') {
|
||||
$showCreateColumnSheet.show = true;
|
||||
$showCreateColumnSheet.title = 'Create column';
|
||||
$showCreateColumnSheet.columns = $tableColumns;
|
||||
$showCreateColumnSheet.columnsOrder = $columnsOrder;
|
||||
}
|
||||
}}>
|
||||
{#if column.isAction}
|
||||
<Spreadsheet.Header.Cell column="actions" {root}>
|
||||
<Button.Button
|
||||
icon
|
||||
variant="extra-compact"
|
||||
onclick={() => {
|
||||
if (mode === 'rows') {
|
||||
$showCreateColumnSheet.show = true;
|
||||
$showCreateColumnSheet.title = 'Create column';
|
||||
$showCreateColumnSheet.columns = $tableColumns;
|
||||
$showCreateColumnSheet.columnsOrder = $columnsOrder;
|
||||
}
|
||||
}}>
|
||||
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
|
||||
</Button.Button>
|
||||
</Spreadsheet.Header.Cell>
|
||||
{:else}
|
||||
<Spreadsheet.Header.Cell
|
||||
{root}
|
||||
column={column.id}
|
||||
@@ -215,7 +304,7 @@
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Spreadsheet.Header.Cell>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -236,48 +325,36 @@
|
||||
{#if !$spreadsheetLoading}
|
||||
<div
|
||||
class="spreadsheet-fade-bottom"
|
||||
class:custom-columns={customColumns.length > 0}
|
||||
data-collapsed-tabs={!$expandTabs}
|
||||
style:--overlay-left={overlayLeftOffset}
|
||||
style:--overlay-top={overlayTopOffset}
|
||||
style:--dynamic-overlay-height={dynamicOverlayHeight}>
|
||||
<div class="empty-actions">
|
||||
<Layout.Stack gap="xl" alignItems="center">
|
||||
<Typography.Title>{title ?? `You have no ${mode} yet`}</Typography.Title>
|
||||
<Layout.Stack
|
||||
gap="xl"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
style="width: 653px; max-width: {$isSmallViewport ? '353px' : undefined}">
|
||||
<Layout.Stack gap="xs" alignItems="center" alignContent="center">
|
||||
<Typography.Title>{title ?? `You have no ${mode} yet`}</Typography.Title>
|
||||
|
||||
{#if showActions}
|
||||
<Layout.Stack
|
||||
inline
|
||||
gap="s"
|
||||
alignItems="center"
|
||||
direction={$isSmallViewport ? 'column' : 'row'}>
|
||||
{#if mode !== 'rows-filtered'}
|
||||
<Button.Button
|
||||
icon
|
||||
size="s"
|
||||
variant="secondary"
|
||||
disabled={actions?.primary?.disabled}
|
||||
onclick={actions?.primary?.onClick}>
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
{actions?.primary?.text ?? `Create ${mode}`}
|
||||
</Button.Button>
|
||||
{@render subtitle?.()}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if mode === 'rows'}
|
||||
<Button.Button
|
||||
size="s"
|
||||
variant="secondary"
|
||||
disabled={actions?.random?.disabled}
|
||||
onclick={actions?.random?.onClick}>
|
||||
{actions?.random?.text ?? `Generate sample data`}
|
||||
</Button.Button>
|
||||
{#if showActions && actions}
|
||||
{@const inline = mode === 'rows-filtered'}
|
||||
<div class="controlled-width">
|
||||
<Layout.Stack {inline}>
|
||||
{#if inline}
|
||||
{@render actions?.()}
|
||||
{:else}
|
||||
<Layout.Grid columns={2} columnsXS={1}>
|
||||
{@render actions?.()}
|
||||
</Layout.Grid>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button.Button
|
||||
size="s"
|
||||
variant="secondary"
|
||||
disabled={actions?.primary?.disabled}
|
||||
onclick={actions?.primary?.onClick}>
|
||||
{actions?.primary?.text}
|
||||
</Button.Button>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
@@ -291,13 +368,59 @@
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
|
||||
& :global(.spreadsheet-container) {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
& :global([data-select='true']) {
|
||||
opacity: 0.85;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.custom-columns {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
&.no-custom-columns {
|
||||
@media (max-width: 768px) {
|
||||
& :global(.spreadsheet-wrapper) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
& > .spreadsheet-fade-bottom {
|
||||
top: var(--top-actions-spacing) !important;
|
||||
background: var(--bgcolor-neutral-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.custom-columns) :global(.spreadsheet-container) {
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* alternative selector for header selection */
|
||||
& :global(.sticky-header [data-select='true']) {
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&[data-mode='rows'] {
|
||||
--top-actions-spacing: 50%;
|
||||
|
||||
& :global([role='rowheader'] :nth-last-child(2) [role='presentation']) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-mode='indexes'] {
|
||||
--top-actions-spacing: 40%;
|
||||
|
||||
& :global([role='cell']:last-child [role='presentation']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& :global([role='rowheader'] [role='cell']:nth-last-child(1)) {
|
||||
pointer-events: none;
|
||||
|
||||
@@ -306,22 +429,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& :global(.spreadsheet-container) {
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
& :global([data-select='true']) {
|
||||
opacity: 0.85;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.spreadsheet-fade-bottom {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: var(--overlay-top, auto);
|
||||
left: var(--overlay-left, 0px);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
@@ -330,17 +445,21 @@
|
||||
);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: none !important;
|
||||
|
||||
height: var(--dynamic-overlay-height, 70.5vh);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
height: var(--dynamic-overlay-height, 63.35vh);
|
||||
&.custom-columns {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
height: var(--dynamic-overlay-height, 70.35vh);
|
||||
.controlled-width {
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
width: 538px;
|
||||
max-width: 538px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,36 +473,19 @@
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
left: 50%;
|
||||
bottom: 35%;
|
||||
position: fixed;
|
||||
|
||||
@media (max-width: 768px) and (max-height: 768px) {
|
||||
left: unset;
|
||||
bottom: 12.5% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) and (max-height: 1024px) {
|
||||
left: unset;
|
||||
bottom: 15% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) and (max-height: 1024px) {
|
||||
left: unset;
|
||||
bottom: 15%;
|
||||
}
|
||||
margin-bottom: 10%;
|
||||
pointer-events: auto;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
left: unset;
|
||||
bottom: 30%;
|
||||
// experiment
|
||||
margin-bottom: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
bottom: 37.5%;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
bottom: 40%;
|
||||
@media (max-width: 768px) {
|
||||
// global but controlled properly!
|
||||
:global(main:has(.no-custom-columns) .console-container) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '$lib/components';
|
||||
import type { ComponentType } from 'svelte';
|
||||
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let {
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
onClick,
|
||||
href,
|
||||
disabled
|
||||
}: {
|
||||
icon?: ComponentType;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
onClick?: () => Promise<void> | void;
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Card
|
||||
{href}
|
||||
{disabled}
|
||||
external
|
||||
radius="m"
|
||||
padding="xs"
|
||||
variant="primary"
|
||||
isButton={!href}
|
||||
on:click={() => onClick?.()}>
|
||||
<Layout.Stack direction="row" gap="m">
|
||||
{#if icon}
|
||||
<Icon {icon} size="m" color="--fgcolor-neutral-tertiary" />
|
||||
{/if}
|
||||
|
||||
<Layout.Stack direction="column" gap="none">
|
||||
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{#if subtitle}
|
||||
<Typography.Text color="--fgcolor-neutral-secondary">
|
||||
{subtitle}
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Card>
|
||||
+8
-1
@@ -898,6 +898,7 @@
|
||||
id={row?.$id}
|
||||
virtualItem={item}
|
||||
select={rowSelection}
|
||||
hoverEffect
|
||||
showSelectOnHover
|
||||
valueWithoutHover={row.$sequence}>
|
||||
{#each $tableColumns as { id: columnId, isEditable } (columnId)}
|
||||
@@ -1159,7 +1160,8 @@
|
||||
gap="xs"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
alignContent="center">
|
||||
alignContent="center"
|
||||
class="footer-input-select-wrapper">
|
||||
<span style:white-space="nowrap"> Page </span>
|
||||
|
||||
<InputSelect
|
||||
@@ -1302,6 +1304,11 @@
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
:global(.footer-input-select-wrapper button.input) {
|
||||
height: 30px;
|
||||
background-color: var(--bgcolor-neutral-primary);
|
||||
}
|
||||
|
||||
// very weird because the library already has this!
|
||||
:global(.virtual-row:has([data-editing-mode='true'])) {
|
||||
z-index: 1 !important;
|
||||
|
||||
+3
-2
@@ -11,7 +11,7 @@
|
||||
import { Fieldset, Layout, Icon, Input, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { IconGithub, IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { ID, Runtime } from '@appwrite.io/console';
|
||||
import { ID, Runtime, Type } from '@appwrite.io/console';
|
||||
import { CustomId } from '$lib/components';
|
||||
import { getIconFromRuntime } from '$lib/stores/runtimes';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
@@ -134,7 +134,8 @@
|
||||
repository: data.repository.name,
|
||||
owner: data.repository.owner,
|
||||
rootDirectory: rootDir || '.',
|
||||
version: latestTag ?? '1.0.0',
|
||||
type: Type.Tag,
|
||||
reference: latestTag ?? '1.0.0',
|
||||
activate: true
|
||||
});
|
||||
|
||||
|
||||
+3
-2
@@ -15,7 +15,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import ProductionBranch from '$lib/components/git/productionBranchFieldset.svelte';
|
||||
import Configuration from './configuration.svelte';
|
||||
import { ID, Runtime, type Models } from '@appwrite.io/console';
|
||||
import { ID, Runtime, Type, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
ConnectBehaviour,
|
||||
NewRepository,
|
||||
@@ -179,7 +179,8 @@
|
||||
repository: data.template.providerRepositoryId || undefined,
|
||||
owner: data.template.providerOwner || undefined,
|
||||
rootDirectory: rt?.providerRootDirectory || undefined,
|
||||
version: data.template.providerVersion || undefined,
|
||||
type: Type.Tag,
|
||||
reference: data.template.providerVersion || undefined,
|
||||
activate: true
|
||||
});
|
||||
|
||||
|
||||
+17
-7
@@ -1,4 +1,4 @@
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, getQuery, pageToOffset } from '$lib/helpers/load';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
@@ -15,17 +15,27 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
|
||||
const parsedQueries = queryParamToMap(query || '[]');
|
||||
queries.set(parsedQueries);
|
||||
let activeDeployment: Models.Deployment | null = null;
|
||||
if (data.function.deploymentId) {
|
||||
try {
|
||||
activeDeployment = await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.functions.getDeployment({
|
||||
functionId: params.function,
|
||||
deploymentId: data.function.deploymentId
|
||||
});
|
||||
} catch (error) {
|
||||
// active deployment with the requested ID could not be found
|
||||
activeDeployment = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
query,
|
||||
installations: data.installations,
|
||||
activeDeployment: data.function.deploymentId
|
||||
? await sdk.forProject(params.region, params.project).functions.getDeployment({
|
||||
functionId: params.function,
|
||||
deploymentId: data.function.deploymentId
|
||||
})
|
||||
: null,
|
||||
activeDeployment,
|
||||
deploymentList: await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.functions.listDeployments({
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
let isSubmitting = writable(false);
|
||||
|
||||
let scopes: string[] = [];
|
||||
let name = '',
|
||||
expire = '';
|
||||
let name = '';
|
||||
let expire: string | null = null;
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { app } from '$lib/stores/app';
|
||||
import Light from '../assets/cursor-ai.svg';
|
||||
import Dark from '../assets/dark/cursor-ai.svg';
|
||||
</script>
|
||||
|
||||
<img src={$app.themeInUse === 'dark' ? Dark : Light} width="20" height="20" alt="Cursor" />
|
||||
+79
-1
@@ -17,7 +17,7 @@
|
||||
import { Card } from '$lib/components';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { realtime, sdk } from '$lib/stores/sdk';
|
||||
import { getApiEndpoint, realtime, sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -26,6 +26,7 @@
|
||||
import { PlatformType } from '@appwrite.io/console';
|
||||
import { project } from '../../store';
|
||||
import { getCorrectTitle, type PlatformProps } from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
|
||||
let { isConnectPlatform = false }: PlatformProps = $props();
|
||||
|
||||
@@ -35,6 +36,60 @@
|
||||
let isPlatformCreated = $state(isConnectPlatform);
|
||||
|
||||
const projectId = page.params.project;
|
||||
const VERSIONS_ENDPOINT = (() => {
|
||||
const endpoint = getApiEndpoint(page.params.region);
|
||||
const url = new URL('/versions', endpoint);
|
||||
return url.toString();
|
||||
})();
|
||||
let androidSdkVersion = $state('11.3.0');
|
||||
|
||||
function buildAndroidInstructions(version: string) {
|
||||
return `
|
||||
Confirm you're working inside the correct Android project before editing anything:
|
||||
- Navigate into the directory that contains the real Android app module (look for gradlew, settings.gradle, and the app-level build.gradle(.kts)).
|
||||
- If Cursor opens in a parent folder (like your home directory) or you see multiple Android projects, ask which one to modify before making changes.
|
||||
- Update the app-level build.gradle.kts by default, but be ready to edit a Groovy build.gradle if the project hasn't migrated to Kotlin DSL yet.
|
||||
|
||||
Prefer Version Catalogs when adding the Appwrite SDK:
|
||||
1. If ./gradle/libs.versions.toml exists, add or reuse an Appwrite entry:
|
||||
\`\`\`toml
|
||||
[libraries]
|
||||
appwrite = { module = "io.appwrite:sdk-for-android", version = "${version}" }
|
||||
\`\`\`
|
||||
2. Reference it inside the module's dependencies block:
|
||||
\`\`\`kotlin
|
||||
dependencies {
|
||||
implementation(libs.appwrite)
|
||||
}
|
||||
\`\`\`
|
||||
Only when the project lacks ./gradle/libs.versions.toml should you hardcode the dependency:
|
||||
\`\`\`kotlin
|
||||
implementation("io.appwrite:sdk-for-android:${version}")
|
||||
\`\`\`
|
||||
Legacy Groovy scripts should use:
|
||||
\`\`\`groovy
|
||||
implementation "io.appwrite:sdk-for-android:${version}"
|
||||
\`\`\`
|
||||
|
||||
Before introducing any new files, search the project (app/src, libs/, shared modules, etc.) for existing Appwrite client helpers (look for \`Client(\`, \`AppwriteClient\`, or \`.setEndpoint\`). If a client already exists, update its configuration instead of creating a duplicate.
|
||||
|
||||
Ensure the Appwrite client is initialized with the application context and current project info:
|
||||
\`\`\`kotlin
|
||||
val client = Client(applicationContext)
|
||||
.setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}")
|
||||
.setProject("${projectId}")
|
||||
|
||||
val account = Account(client)
|
||||
\`\`\`
|
||||
|
||||
From the app's entry point (e.g., Application class or the first launched Activity), automatically invoke a helper that pings Appwrite so the user can verify connectivity and will be reflected on the Appwrite console:
|
||||
\`\`\`kotlin
|
||||
client.ping()
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
|
||||
const alreadyExistsInstructions = $derived(buildAndroidInstructions(androidSdkVersion));
|
||||
|
||||
const gitCloneCode =
|
||||
'\ngit clone https://github.com/appwrite/starter-for-android\ncd starter-for-android\n';
|
||||
@@ -43,6 +98,22 @@
|
||||
const val APPWRITE_PROJECT_NAME = "${$project.name}"
|
||||
const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}"`;
|
||||
|
||||
async function fetchAndroidSdkVersion() {
|
||||
try {
|
||||
const response = await fetch(VERSIONS_ENDPOINT);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch versions: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const latestVersion = data?.['client-android'];
|
||||
if (typeof latestVersion === 'string' && latestVersion.trim()) {
|
||||
androidSdkVersion = latestVersion.trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unable to fetch latest Android SDK version', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function createAndroidPlatform() {
|
||||
try {
|
||||
isCreatingPlatform = true;
|
||||
@@ -83,6 +154,7 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchAndroidSdkVersion();
|
||||
const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => {
|
||||
if (response.events.includes(`projects.${projectId}.ping`)) {
|
||||
connectionSuccessful = true;
|
||||
@@ -171,6 +243,12 @@ const val APPWRITE_PUBLIC_ENDPOINT = "${sdk.forProject(page.params.region, page.
|
||||
{#if isPlatformCreated}
|
||||
<Fieldset legend="Clone starter" badge="Optional">
|
||||
<Layout.Stack gap="l">
|
||||
<LlmBanner
|
||||
platform="android"
|
||||
{configCode}
|
||||
{alreadyExistsInstructions}
|
||||
openers={['cursor']} />
|
||||
|
||||
<Typography.Text variant="m-500">
|
||||
1. If you're starting a new project, you can clone our starter kit from
|
||||
GitHub using the terminal, VSCode or Android Studio.
|
||||
|
||||
+32
-1
@@ -28,6 +28,7 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import { project } from '../../store';
|
||||
import { getCorrectTitle, type PlatformProps } from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
|
||||
let { isConnectPlatform = false, platform = PlatformType.Appleios }: PlatformProps = $props();
|
||||
|
||||
@@ -38,6 +39,30 @@
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
const alreadyExistsInstructions = `
|
||||
Install the Appwrite iOS SDK using the following package URL:
|
||||
|
||||
\`\`\`
|
||||
https://github.com/appwrite/sdk-for-apple
|
||||
\`\`\`
|
||||
|
||||
From a suitable lib directory, export the Appwrite client as a global variable:
|
||||
|
||||
\`\`\`
|
||||
let client = Client()
|
||||
.setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}")
|
||||
.setProject("${projectId}")
|
||||
|
||||
let account = Account(client)
|
||||
\`\`\`
|
||||
|
||||
On the homepage of the app, create a button that says "Send a ping" and when clicked, it should call the following function:
|
||||
|
||||
\`\`\`
|
||||
client.ping()
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const gitCloneCode =
|
||||
'\ngit clone https://github.com/appwrite/starter-for-ios\ncd starter-for-ios\n';
|
||||
|
||||
@@ -45,7 +70,7 @@
|
||||
APPWRITE_PROJECT_NAME: "${$project.name}"
|
||||
APPWRITE_PUBLIC_ENDPOINT: "${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}"`;
|
||||
|
||||
let platforms: { [key: string]: PlatformType } = {
|
||||
const platforms: { [key: string]: PlatformType } = {
|
||||
iOS: PlatformType.Appleios,
|
||||
macOS: PlatformType.Applemacos,
|
||||
watchOS: PlatformType.Applewatchos,
|
||||
@@ -199,6 +224,12 @@ APPWRITE_PUBLIC_ENDPOINT: "${sdk.forProject(page.params.region, page.params.proj
|
||||
{#if isPlatformCreated}
|
||||
<Fieldset legend="Clone starter" badge="Optional">
|
||||
<Layout.Stack gap="l">
|
||||
<LlmBanner
|
||||
platform="apple"
|
||||
{configCode}
|
||||
{alreadyExistsInstructions}
|
||||
openers={['cursor']} />
|
||||
|
||||
<Typography.Text variant="m-500">
|
||||
1. If you're starting a new project, you can clone our starter kit from
|
||||
GitHub using the terminal or XCode.
|
||||
|
||||
+56
-1
@@ -18,7 +18,7 @@
|
||||
import { Card } from '$lib/components';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { realtime, sdk } from '$lib/stores/sdk';
|
||||
import { getApiEndpoint, realtime, sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -27,6 +27,7 @@
|
||||
import { PlatformType } from '@appwrite.io/console';
|
||||
import { project } from '../../store';
|
||||
import { getCorrectTitle, type PlatformProps } from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
|
||||
let { isConnectPlatform = false, platform = PlatformType.Flutterandroid }: PlatformProps =
|
||||
$props();
|
||||
@@ -37,6 +38,38 @@
|
||||
let isPlatformCreated = $state(isConnectPlatform);
|
||||
|
||||
const projectId = page.params.project;
|
||||
const VERSIONS_ENDPOINT = (() => {
|
||||
const endpoint = getApiEndpoint(page.params.region);
|
||||
const url = new URL('/versions', endpoint);
|
||||
return url.toString();
|
||||
})();
|
||||
let flutterSdkVersion = $state('20.3.0');
|
||||
|
||||
function buildFlutterInstructions(version: string) {
|
||||
return `
|
||||
Install the Appwrite Flutter SDK using the following command:
|
||||
|
||||
\`\`\`
|
||||
flutter pub add appwrite:${version}
|
||||
\`\`\`
|
||||
|
||||
From a suitable lib directory, export the Appwrite client as a global variable, hardcode the project details too:
|
||||
|
||||
\`\`\`
|
||||
final Client client = Client()
|
||||
.setProject("${projectId}")
|
||||
.setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}");
|
||||
\`\`\`
|
||||
|
||||
On the homepage of the app, create a button that says "Send a ping" and when clicked, it should call the following function:
|
||||
|
||||
\`\`\`
|
||||
client.ping();
|
||||
\`\`\`
|
||||
`;
|
||||
}
|
||||
|
||||
const alreadyExistsInstructions = $derived(buildFlutterInstructions(flutterSdkVersion));
|
||||
|
||||
const gitCloneCode =
|
||||
'\ngit clone https://github.com/appwrite/starter-for-flutter\ncd starter-for-flutter\n';
|
||||
@@ -111,6 +144,22 @@
|
||||
[PlatformType.Flutterwindows]: 'Package name'
|
||||
};
|
||||
|
||||
async function fetchFlutterSdkVersion() {
|
||||
try {
|
||||
const response = await fetch(VERSIONS_ENDPOINT);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch versions: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const latestVersion = data?.['client-flutter'];
|
||||
if (typeof latestVersion === 'string' && latestVersion.trim()) {
|
||||
flutterSdkVersion = latestVersion.trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unable to fetch latest Flutter SDK version', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function createFlutterPlatform() {
|
||||
try {
|
||||
isCreatingPlatform = true;
|
||||
@@ -158,6 +207,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchFlutterSdkVersion();
|
||||
const unsubscribe = realtime.forConsole(page.params.region, 'console', (response) => {
|
||||
if (response.events.includes(`projects.${projectId}.ping`)) {
|
||||
connectionSuccessful = true;
|
||||
@@ -281,6 +331,11 @@
|
||||
{#if isPlatformCreated}
|
||||
<Fieldset legend="Clone starter" badge="Optional">
|
||||
<Layout.Stack gap="l">
|
||||
<LlmBanner
|
||||
platform="flutter"
|
||||
{configCode}
|
||||
{alreadyExistsInstructions}
|
||||
openers={['cursor']} />
|
||||
<Typography.Text variant="m-500">
|
||||
1. If you're starting a new project, you can clone our starter kit from
|
||||
GitHub using the terminal, VSCode or Android Studio.
|
||||
|
||||
+35
@@ -27,6 +27,7 @@
|
||||
import { PlatformType } from '@appwrite.io/console';
|
||||
import { project } from '../../store';
|
||||
import { getCorrectTitle, type PlatformProps } from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
|
||||
let { isConnectPlatform = false, platform = PlatformType.Reactnativeandroid }: PlatformProps =
|
||||
$props();
|
||||
@@ -38,6 +39,28 @@
|
||||
|
||||
const projectId = page.params.project;
|
||||
|
||||
const alreadyExistsInstructions = `
|
||||
Install the Appwrite React Native SDK using the following command, respect user's package manager of choice and use the one being used in the codebase:
|
||||
|
||||
\`\`\`
|
||||
npx expo install react-native-appwrite react-native-url-polyfill
|
||||
\`\`\`
|
||||
|
||||
From a suitable lib directory, export the Appwrite client as a global variable, hardcode the project details too:
|
||||
|
||||
\`\`\`
|
||||
const client = new Client()
|
||||
.setProject("${projectId}")
|
||||
.setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}");
|
||||
\`\`\`
|
||||
|
||||
From the entrypoint of the app, make it so that the following function is automatically called which will ping the Appwrite backend server to verify the setup. Let the user know about this function being added
|
||||
|
||||
\`\`\`
|
||||
client.ping();
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const gitCloneCode =
|
||||
'\ngit clone https://github.com/appwrite/starter-for-react-native\ncd starter-for-react-native\n';
|
||||
|
||||
@@ -45,6 +68,12 @@
|
||||
EXPO_PUBLIC_APPWRITE_PROJECT_NAME="${$project.name}"
|
||||
EXPO_PUBLIC_APPWRITE_ENDPOINT=${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}`;
|
||||
|
||||
const promptConfigCode = `
|
||||
const client = new Client()
|
||||
.setProject("${projectId}")
|
||||
.setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}")
|
||||
`;
|
||||
|
||||
let platforms: { [key: string]: PlatformType } = {
|
||||
Android: PlatformType.Reactnativeandroid,
|
||||
iOS: PlatformType.Reactnativeios
|
||||
@@ -225,6 +254,12 @@ EXPO_PUBLIC_APPWRITE_ENDPOINT=${sdk.forProject(page.params.region, page.params.p
|
||||
{#if isPlatformCreated}
|
||||
<Fieldset legend="Clone starter" badge="Optional">
|
||||
<Layout.Stack gap="l">
|
||||
<LlmBanner
|
||||
platform="reactnative"
|
||||
configCode={promptConfigCode}
|
||||
{alreadyExistsInstructions}
|
||||
openers={['cursor']} />
|
||||
|
||||
<Typography.Text variant="m-500">
|
||||
1. If you're starting a new project, you can clone our starter kit from
|
||||
GitHub using the terminal or VSCode.
|
||||
|
||||
+60
-1
@@ -48,7 +48,13 @@
|
||||
} from './components/index';
|
||||
import { extendedHostnameRegex } from '$lib/helpers/string';
|
||||
import { project } from '../../store';
|
||||
import { type PlatformProps, type FrameworkType, getCorrectTitle } from './store';
|
||||
import {
|
||||
type PlatformProps,
|
||||
type FrameworkType,
|
||||
type LLMPromptConfig,
|
||||
getCorrectTitle
|
||||
} from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
|
||||
let { key, isConnectPlatform = false, platform = PlatformType.Web }: PlatformProps = $props();
|
||||
|
||||
@@ -157,6 +163,57 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p
|
||||
selectedFramework ? selectedFramework.icon : NoFrameworkIcon
|
||||
);
|
||||
|
||||
const llmConfig: LLMPromptConfig = $derived({
|
||||
alreadyExistsInstructions: `
|
||||
Install the Appwrite web SDK using the following command. Respect the user's package manager of choice. Do not use NPM if the user uses Bun for example.
|
||||
|
||||
\`\`\`bash
|
||||
npm install appwrite
|
||||
\`\`\`
|
||||
|
||||
Create a new \`appwrite.js\` (or equivalent, respecting the framework and language, don't create a JS file if TS is being used in the project) file in a suitable lib directory and have the following code:
|
||||
|
||||
\`\`\`js
|
||||
import { Client, Account, Databases } from "appwrite";
|
||||
|
||||
const client = new Client()
|
||||
.setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}")
|
||||
.setProject("${projectId}");
|
||||
|
||||
const account = new Account(client);
|
||||
const databases = new Databases(client);
|
||||
|
||||
export { client, account, databases };
|
||||
\`\`\`
|
||||
|
||||
When the app is opened, make it so that the following function is automatically called which will ping the Appwrite backend server to verify the setup. Let the user know about this function being added
|
||||
|
||||
\`\`\`js
|
||||
client.ping();
|
||||
\`\`\`
|
||||
`,
|
||||
title: `Copy prompt: starter kit for Appwrite in ${selectedFramework?.label || 'Web'}`,
|
||||
cloneCommand: `git clone https://github.com/appwrite/starter-for-${selectedFramework?.key}\ncd starter-for-${selectedFramework?.key}`,
|
||||
configFile:
|
||||
selectedFramework?.key === 'angular'
|
||||
? 'src/environments/environment.ts'
|
||||
: 'appwrite.js',
|
||||
configCode:
|
||||
// selectedFramework?.key === 'angular'
|
||||
// ? `APPWRITE_PROJECT_ID=${projectId}\nAPPWRITE_PROJECT_NAME=${$project.name}\nAPPWRITE_ENDPOINT=${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}`
|
||||
// : `
|
||||
// const client = new Client()
|
||||
// .setEndpoint("${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}")
|
||||
// .setProject("${projectId}");
|
||||
// `,
|
||||
`APPWRITE_PROJECT_ID = "${projectId}"
|
||||
APPWRITE_PROJECT_NAME = "${$project.name}"
|
||||
APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.project).client.config.endpoint}"`,
|
||||
configLanguage: selectedFramework?.key === 'angular' ? 'ts' : 'dotenv',
|
||||
runInstructions: `Install project dependencies using \`npm install\`, then run the app using \`${selectedFramework?.runCommand}\`. Demo app runs on http://localhost:${selectedFramework?.portNumber}. Click the \`Send a ping\` button to verify the setup.`,
|
||||
using: 'the terminal or VSCode'
|
||||
});
|
||||
|
||||
async function createWebPlatform() {
|
||||
const hostnameRegex = new RegExp(extendedHostnameRegex);
|
||||
const finalHostname = hostname?.trim() || 'localhost';
|
||||
@@ -301,6 +358,8 @@ ${prefix}APPWRITE_ENDPOINT = "${sdk.forProject(page.params.region, page.params.p
|
||||
{#if isPlatformCreated && !isChangingFramework}
|
||||
<Fieldset legend="Clone starter" badge="Optional">
|
||||
<Layout.Stack gap="l">
|
||||
<LlmBanner config={llmConfig} openers={['cursor', 'lovable']} />
|
||||
|
||||
<Typography.Text variant="m-500">
|
||||
1. If you're starting a new project, you can clone our starter kit from
|
||||
GitHub using the terminal or VSCode.
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<script lang="ts">
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
ActionMenu,
|
||||
Alert,
|
||||
Icon,
|
||||
Layout,
|
||||
Popover,
|
||||
Typography,
|
||||
Button as PinkButton
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { IconChevronDown, IconChevronUp, IconLovable } from '@appwrite.io/pink-icons-svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { buildPlatformConfig, generatePromptFromConfig, type LLMPromptConfig } from './store';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import IconAINotification from '../../databases/database-[database]/(suggestions)/icon/aiNotification.svelte';
|
||||
import Avatar from '$lib/components/avatar.svelte';
|
||||
import CursorIcon from '$routes/(console)/project-[region]-[project]/overview/components/CursorIconLarge.svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
let {
|
||||
platform,
|
||||
configCode,
|
||||
alreadyExistsInstructions,
|
||||
config: customConfig,
|
||||
openers = [] as Array<SupportedAgents>
|
||||
}: {
|
||||
platform?: string;
|
||||
configCode?: string;
|
||||
alreadyExistsInstructions?: string;
|
||||
config?: LLMPromptConfig;
|
||||
openers?: Array<SupportedAgents>;
|
||||
} = $props();
|
||||
|
||||
const config = $derived.by(() => {
|
||||
if (customConfig) return customConfig;
|
||||
if (platform && configCode)
|
||||
return buildPlatformConfig(platform, configCode, alreadyExistsInstructions);
|
||||
throw new Error('LlmBanner: must provide either config OR (platform + configCode)');
|
||||
});
|
||||
|
||||
const prompt = $derived(generatePromptFromConfig(config));
|
||||
|
||||
let showAlert = $state(true);
|
||||
|
||||
type OpenerConfig = {
|
||||
id: SupportedAgents;
|
||||
label: string;
|
||||
description: string;
|
||||
href: (prompt: string) => string;
|
||||
icon?: ComponentType;
|
||||
imgSrc?: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
type SupportedAgents = 'cursor' | 'lovable';
|
||||
|
||||
const openersConfig: Record<SupportedAgents, OpenerConfig> = {
|
||||
cursor: {
|
||||
id: 'cursor',
|
||||
label: 'Open in Cursor',
|
||||
description: 'Set up starter kit in Cursor',
|
||||
href: (p: string) => {
|
||||
trackEvent(Click.OpenInCursorClick, {
|
||||
platform: config.title
|
||||
});
|
||||
const u = new URL('https://cursor.com/link/prompt');
|
||||
u.searchParams.set('text', p);
|
||||
return u.toString();
|
||||
},
|
||||
icon: CursorIcon,
|
||||
alt: 'Cursor'
|
||||
},
|
||||
lovable: {
|
||||
id: 'lovable',
|
||||
label: 'Open in Lovable',
|
||||
description: 'Set up starter kit in Lovable',
|
||||
href: (p: string) => {
|
||||
trackEvent(Click.OpenInLovableClick, {
|
||||
platform: config.title
|
||||
});
|
||||
const u = new URL('https://lovable.dev/');
|
||||
u.searchParams.set('autosubmit', 'true');
|
||||
u.searchParams.set('prompt', p);
|
||||
return u.toString();
|
||||
},
|
||||
icon: IconLovable,
|
||||
alt: 'Lovable'
|
||||
}
|
||||
};
|
||||
|
||||
const validOpeners = $derived(openers.filter((id) => openersConfig[id]));
|
||||
|
||||
async function copyPrompt() {
|
||||
await copy(prompt);
|
||||
|
||||
trackEvent(Click.CopyPromptStarterKitClick, {
|
||||
platform: config.title
|
||||
});
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Prompt copied to clipboard'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showAlert}
|
||||
<Alert.Inline
|
||||
status="info"
|
||||
title="Set up your starter kit with AI"
|
||||
dismissible
|
||||
--bgcolor-neutral-default="var(--bgcolor-neutral-primary)"
|
||||
--fgcolor-info="var(--fgcolor-neutral-primary)"
|
||||
on:dismiss={() => (showAlert = false)}>
|
||||
<svelte:fragment slot="icon">
|
||||
<IconAINotification />
|
||||
</svelte:fragment>
|
||||
<Layout.Stack direction="column" class="alert-content" gap="l">
|
||||
<Layout.Stack direction="column" alignItems="center" gap="s">
|
||||
<Typography.Text>
|
||||
Copy the prompt or open it directly in an AI tool like Cursor or Lovable to get
|
||||
step-by-step instructions, starter code, and SDK commands for your project.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Popover let:toggle let:showing padding="none" placement="bottom-start">
|
||||
<svelte:fragment slot="tooltip" let:toggle>
|
||||
<ActionMenu.Root>
|
||||
{#each validOpeners as openerId}
|
||||
{@const o = openersConfig[openerId]}
|
||||
{#if o}
|
||||
<ActionMenu.Item.Button
|
||||
on:click={(e) => {
|
||||
window.open(
|
||||
o.href(prompt),
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
);
|
||||
toggle(e);
|
||||
}}>
|
||||
<Layout.Stack direction="row" gap="s" alignItems="center">
|
||||
<Avatar size="s" alt={o.alt}>
|
||||
{#if o.icon}
|
||||
<Icon icon={o.icon} size="l" />
|
||||
{:else if o.imgSrc}
|
||||
<img src={o.imgSrc} alt={o.alt} />
|
||||
{/if}
|
||||
</Avatar>
|
||||
<Layout.Stack gap="none">
|
||||
<Typography.Text
|
||||
color="--fgcolor-neutral-secondary"
|
||||
variant="m-500">{o.label}</Typography.Text>
|
||||
<Typography.Text color="--fgcolor-neutral-tertiary">
|
||||
{o.description}
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
<PinkButton.Split>
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
class={validOpeners.length ? 'btn-no-right-radius' : ''}
|
||||
on:click={copyPrompt}
|
||||
disabled={!prompt || prompt.length === 0}>Copy setup prompt</Button>
|
||||
{#if validOpeners.length}
|
||||
<Button
|
||||
secondary
|
||||
size="s"
|
||||
class="btn-no-left-radius"
|
||||
icon
|
||||
on:click={toggle}
|
||||
ariaLabel="Open action menu"
|
||||
disabled={!prompt || prompt.length === 0}>
|
||||
<Icon icon={showing ? IconChevronUp : IconChevronDown} />
|
||||
</Button>
|
||||
{/if}
|
||||
</PinkButton.Split>
|
||||
</Popover>
|
||||
</Layout.Stack>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
@@ -17,6 +17,119 @@ export type FrameworkType = {
|
||||
updateConfigCode: string;
|
||||
};
|
||||
|
||||
export type LLMPromptConfig = {
|
||||
title: string;
|
||||
alreadyExistsInstructions: string;
|
||||
cloneCommand: string;
|
||||
configFile: string;
|
||||
configCode: string;
|
||||
configLanguage: string;
|
||||
runInstructions: string;
|
||||
using: string;
|
||||
};
|
||||
|
||||
export function getCorrectTitle(isConnectPlatform: boolean, platform: string) {
|
||||
return isConnectPlatform ? `Connect your ${platform} app` : `Add ${platform} platform`;
|
||||
}
|
||||
|
||||
export function generatePromptFromConfig(config: LLMPromptConfig): string {
|
||||
return `
|
||||
Goal: Setting up Appwrite SDK in the project depending on if a project already exists or not.
|
||||
|
||||
Following are the project details:
|
||||
|
||||
\`\`\`
|
||||
${config.configCode}
|
||||
\`\`\`
|
||||
|
||||
Follow the steps depending on if a project already exists on user's working directory or not:
|
||||
|
||||
## If a project already exists:
|
||||
${config.alreadyExistsInstructions}
|
||||
|
||||
## If a project does not exist:
|
||||
|
||||
1. Clone the starter kit using ${config.using || 'the terminal'}. Make sure to clone in the current working directory so that the cloned files are directly available in the working directory.
|
||||
|
||||
\`\`\`bash
|
||||
${config.cloneCommand} .
|
||||
\`\`\`
|
||||
|
||||
2. Replace all occurrences of the environment variables described in the project details section with their corresponding values. This effectively hardcodes the project details wherever those environment variables are used. Use grep (or an equivalent search) to find and update all occurrences.
|
||||
3. ${config.runInstructions}`;
|
||||
}
|
||||
|
||||
type PlatformConfig = {
|
||||
name: string;
|
||||
title: string;
|
||||
repoName: string;
|
||||
configFile: string;
|
||||
configLanguage: string;
|
||||
runInstructions: string;
|
||||
using: string;
|
||||
};
|
||||
|
||||
const platformConfigs: Record<string, PlatformConfig> = {
|
||||
android: {
|
||||
name: 'Kotlin',
|
||||
title: 'Copy prompt: starter kit for Appwrite in Kotlin',
|
||||
repoName: 'starter-for-android',
|
||||
configFile: 'constants/AppwriteConfig.kt',
|
||||
configLanguage: 'kotlin',
|
||||
runInstructions:
|
||||
'Run the app on a connected device or emulator, then click the `Send a ping` button to verify the setup.',
|
||||
using: 'the terminal, VSCode or Android Studio'
|
||||
},
|
||||
apple: {
|
||||
name: 'Apple platforms',
|
||||
title: 'Copy prompt: starter kit for Appwrite for Apple platforms',
|
||||
repoName: 'starter-for-ios',
|
||||
configFile: 'Sources/Config.plist',
|
||||
configLanguage: 'plaintext',
|
||||
runInstructions:
|
||||
'Run the app on a connected device or simulator, then click the `Send a ping` button to verify the setup.',
|
||||
using: 'the terminal or XCode'
|
||||
},
|
||||
flutter: {
|
||||
name: 'Flutter',
|
||||
title: 'Copy prompt: starter kit for Appwrite in Flutter',
|
||||
repoName: 'starter-for-flutter',
|
||||
configFile: 'lib/config/environment.dart',
|
||||
configLanguage: 'dart',
|
||||
runInstructions:
|
||||
'Run the app on a connected device or simulator using `flutter run -d [device_name]`, then click the `Send a ping` button to verify the setup. Ask the user if the AI agent should run the command to run the app for them. Provide the full command while you ask for permission.',
|
||||
using: 'the terminal'
|
||||
},
|
||||
reactnative: {
|
||||
name: 'React Native',
|
||||
title: 'Copy prompt: starter kit for Appwrite in React Native',
|
||||
repoName: 'starter-for-react-native',
|
||||
configFile: 'index.ts',
|
||||
configLanguage: 'typescript',
|
||||
runInstructions:
|
||||
'After replacing and hardcoding project details, run the app on a connected device or simulator using `npm install` followed by `npm run ios` or `npm run android`, then click the `Send a ping` button to verify the setup. Ask the user if the AI agent should run the command to run the app for them. Provide the full command while you ask for permission.',
|
||||
using: 'the terminal or VSCode'
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPlatformConfig(
|
||||
platformKey: string,
|
||||
configCode: string,
|
||||
alreadyExistsInstructions: string
|
||||
): LLMPromptConfig {
|
||||
const config = platformConfigs[platformKey];
|
||||
if (!config) {
|
||||
throw new Error(`Unknown platform: ${platformKey}`);
|
||||
}
|
||||
|
||||
return {
|
||||
title: config.title,
|
||||
alreadyExistsInstructions: alreadyExistsInstructions,
|
||||
cloneCommand: `git clone https://github.com/appwrite/${config.repoName}\ncd ${config.repoName}`,
|
||||
configFile: config.configFile,
|
||||
configCode: configCode,
|
||||
configLanguage: config.configLanguage,
|
||||
runInstructions: config.runInstructions,
|
||||
using: config.using
|
||||
};
|
||||
}
|
||||
|
||||
+3
-2
@@ -12,7 +12,7 @@
|
||||
import { IconGithub, IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import Domain from '../domain.svelte';
|
||||
import { Adapter, BuildRuntime, Framework, ID } from '@appwrite.io/console';
|
||||
import { Adapter, BuildRuntime, Framework, ID, Type } from '@appwrite.io/console';
|
||||
import { CustomId } from '$lib/components';
|
||||
import { getFrameworkIcon } from '$lib/stores/sites';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
@@ -172,7 +172,8 @@
|
||||
repository: data.repository.name,
|
||||
owner: data.repository.owner,
|
||||
rootDirectory: rootDir || '.',
|
||||
version: latestTag ?? '1.0.0',
|
||||
type: Type.Tag,
|
||||
reference: latestTag ?? '1.0.0',
|
||||
activate: true
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user