mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #2610 from appwrite/patch-e2es
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS="
|
||||
"PUBLIC_APPWRITE_MULTI_REGION=true"
|
||||
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
|
||||
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
|
||||
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
build-args: |
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_APPWRITE_MULTI_REGION=false"
|
||||
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
|
||||
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
|
||||
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS="
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
|
||||
|
||||
@@ -17,21 +17,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
@@ -12,7 +12,7 @@
|
||||
"clean": "rm -rf node_modules && rm -rf .svelte_kit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --cache --write --cache .",
|
||||
"format": "prettier --write --cache .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test": "TZ=EST vitest run",
|
||||
"test:ui": "TZ=EST vitest --ui",
|
||||
@@ -22,11 +22,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5ce6dc7",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@406d8be",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@5b26bb8",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^10.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
timeout: 120000,
|
||||
timeout: 240000,
|
||||
reportSlowTests: null,
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
retries: 3,
|
||||
@@ -11,10 +11,11 @@ const config: PlaywrightTestConfig = {
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
webServer: {
|
||||
timeout: 120000,
|
||||
timeout: 240000,
|
||||
env: {
|
||||
PUBLIC_CONSOLE_PROFILE: '',
|
||||
PUBLIC_AI_SERVICE_BASE_URL: '',
|
||||
NODE_OPTIONS: '--max_old_space_size=8192',
|
||||
PUBLIC_CONSOLE_PROFILE: 'console',
|
||||
PUBLIC_AI_SERVICE_BASE_URL: 'http://appwrite.test/v1',
|
||||
PUBLIC_APPWRITE_ENDPOINT: 'https://stage.cloud.appwrite.io/v1',
|
||||
PUBLIC_CONSOLE_MODE: 'cloud',
|
||||
PUBLIC_APPWRITE_MULTI_REGION: 'true',
|
||||
|
||||
Generated
+281
-274
File diff suppressed because it is too large
Load Diff
@@ -195,7 +195,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 {
|
||||
@@ -357,6 +360,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',
|
||||
|
||||
@@ -32,12 +32,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { dev } from '$app/environment';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { portal } from '$lib/actions/portal';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import {
|
||||
commandCenterKeyDownHandler,
|
||||
disableCommands,
|
||||
isTargetInputLike,
|
||||
registerCommands
|
||||
} from './commands';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { commandCenterKeyDownHandler, disableCommands, registerCommands } from './commands';
|
||||
|
||||
let debugOverlayEnabled = false;
|
||||
|
||||
@@ -99,20 +104,11 @@
|
||||
keys = [];
|
||||
}, 1000);
|
||||
|
||||
function isInputEvent(event: KeyboardEvent) {
|
||||
if (
|
||||
event.target instanceof HTMLElement &&
|
||||
customElements.get(event.target.tagName.toLowerCase())
|
||||
)
|
||||
return true;
|
||||
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
|
||||
}
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
// check if in any webcomponent
|
||||
|
||||
if (!$subPanels.length) {
|
||||
if (isInputEvent(e)) return;
|
||||
if (isTargetInputLike(e.target)) return;
|
||||
keys = [...keys, e.key].slice(-10);
|
||||
resetKeys();
|
||||
}
|
||||
|
||||
@@ -100,13 +100,13 @@ const commandsEnabled = derived(disabledMap, ($disabledMap) => {
|
||||
return Array.from($disabledMap.values()).every((disabled) => !disabled);
|
||||
});
|
||||
|
||||
function isInputEvent(event: KeyboardEvent) {
|
||||
if (
|
||||
event.target instanceof HTMLElement &&
|
||||
customElements.get(event.target.tagName.toLowerCase())
|
||||
)
|
||||
export function isTargetInputLike(element: EventTarget | null) {
|
||||
if (!(element instanceof HTMLElement)) return false;
|
||||
if (element instanceof HTMLElement && customElements.get(element.tagName.toLowerCase()))
|
||||
return true;
|
||||
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
|
||||
return !!element.closest(
|
||||
'input,textarea,select,[contenteditable],[role="combobox"],[role="textbox"],[role="searchbox"],[data-command-center-ignore]'
|
||||
);
|
||||
}
|
||||
|
||||
function getCommandRank(command: KeyedCommand) {
|
||||
@@ -209,7 +209,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -41,6 +39,7 @@
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
import type { Organization } from '$lib/stores/organization';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { formatName as formatNameHelper } from '$lib/helpers/string';
|
||||
|
||||
// props
|
||||
interface Props {
|
||||
@@ -51,8 +50,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);
|
||||
@@ -96,7 +96,7 @@
|
||||
function isUnarchiveDisabled(): boolean {
|
||||
if (!organization || !currentPlan) return true;
|
||||
|
||||
if (isFreePlan(organization.billingPlan)) {
|
||||
if (isPlanBelowPro) {
|
||||
const currentProjectCount = organization.projects?.length || 0;
|
||||
const projectLimit = currentPlan.projects || 0;
|
||||
|
||||
@@ -187,8 +187,6 @@
|
||||
return $regionsStore.regions.find((region) => region.$id === project.region);
|
||||
}
|
||||
|
||||
import { formatName as formatNameHelper } from '$lib/helpers/string';
|
||||
import { isFreePlan } from '$lib/helpers/billing';
|
||||
function formatName(name: string, limit: number = 19) {
|
||||
return formatNameHelper(name, limit, $isSmallViewport);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { createPopper, type Instance } from '@popperjs/core';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { createEventDispatcher, onDestroy, onMount, hasContext } from 'svelte';
|
||||
|
||||
export let show = false;
|
||||
export let noArrow = false;
|
||||
@@ -17,6 +17,10 @@
|
||||
export let display = 'block';
|
||||
export let arrowSize = 10;
|
||||
export let isPopover = false;
|
||||
export let portal = false;
|
||||
|
||||
const inModal = hasContext('dialog-group');
|
||||
const shouldPortalTooltip = portal || inModal;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
blur: undefined;
|
||||
@@ -27,6 +31,22 @@
|
||||
let arrow: HTMLDivElement;
|
||||
let instance: Instance;
|
||||
|
||||
function portalAction(node: HTMLElement) {
|
||||
if (!shouldPortalTooltip) return;
|
||||
|
||||
const bodyElement = document.body;
|
||||
const target = inModal ? node.closest('dialog[open]') || bodyElement : bodyElement;
|
||||
target.appendChild(node);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
if (node.parentElement === target) {
|
||||
target.removeChild(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
instance = createPopper(element, tooltip, {
|
||||
placement,
|
||||
@@ -116,6 +136,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
use:portalAction
|
||||
class="drop-tooltip"
|
||||
bind:this={tooltip}
|
||||
class:u-width-full-line={fullWidth}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let paddingInline: string = undefined;
|
||||
export let resetListPadding: boolean = false;
|
||||
export let gap: string = undefined;
|
||||
export let portal = false;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
</script>
|
||||
@@ -31,6 +32,7 @@
|
||||
{fullWidth}
|
||||
{wrapperFullWidth}
|
||||
{fixed}
|
||||
{portal}
|
||||
on:blur>
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
+43
-3
@@ -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'
|
||||
},
|
||||
@@ -466,6 +467,45 @@ export const scopes: {
|
||||
}
|
||||
];
|
||||
|
||||
export const cloudOnlyBackupScopes = [
|
||||
{
|
||||
scope: 'policies.read',
|
||||
description: 'Access to read your database backup policies',
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'policies.write',
|
||||
description: 'Access to create, update and delete your backup policies',
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'archives.read',
|
||||
description: 'Access to read your database backup archives',
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'archives.write',
|
||||
description: 'Access to create and delete your backup archives',
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'restorations.read',
|
||||
description: 'Access to read your backup restorations',
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
},
|
||||
{
|
||||
scope: 'restorations.write',
|
||||
description: 'Access to create backup restorations',
|
||||
category: 'Database',
|
||||
icon: 'database'
|
||||
}
|
||||
];
|
||||
|
||||
export type EventService = {
|
||||
name: string;
|
||||
resources: EventResource[];
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
helper={error ?? helper}
|
||||
{required}
|
||||
state={error ? 'error' : 'default'}
|
||||
data-command-center-ignore
|
||||
on:invalid={handleInvalid}
|
||||
on:input
|
||||
on:change
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BillingPlan } from '@appwrite.io/console';
|
||||
|
||||
export function isFreePlan(plan: BillingPlan | string): boolean {
|
||||
switch (plan) {
|
||||
case BillingPlan.Imaginebasic:
|
||||
case BillingPlan.Imaginetier0:
|
||||
case BillingPlan.Tier0:
|
||||
return true;
|
||||
default:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 834 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 856 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 221 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@@ -113,7 +113,7 @@ export const studio: Profile = {
|
||||
id: ProfileMode.STUDIO,
|
||||
platform: 'Imagine',
|
||||
organizationPlatform: Platform.Imagine,
|
||||
freeTier: BillingPlan.Imaginebasic,
|
||||
freeTier: BillingPlan.Imaginetier0,
|
||||
logo: {
|
||||
src: {
|
||||
dark: asset('/images/imagine-logo-dark.svg'),
|
||||
|
||||
+16
-3
@@ -491,7 +491,7 @@ export class Billing {
|
||||
billingPlan: string,
|
||||
paymentMethodId: string,
|
||||
platform: Platform,
|
||||
billingAddressId: string = null,
|
||||
billingAddressId: string = undefined,
|
||||
couponId: string = null,
|
||||
invites: Array<string> = [],
|
||||
budget: number = undefined,
|
||||
@@ -630,6 +630,7 @@ export class Billing {
|
||||
budget,
|
||||
taxId
|
||||
};
|
||||
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'patch',
|
||||
@@ -936,12 +937,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',
|
||||
|
||||
@@ -40,8 +40,8 @@ import { user } from './user';
|
||||
import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte';
|
||||
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
|
||||
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
|
||||
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import { isFreePlan } from '$lib/helpers/billing';
|
||||
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
|
||||
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1';
|
||||
|
||||
@@ -101,7 +101,7 @@ export function tierToPlan(tier: Tier) {
|
||||
case BillingPlan.ENTERPRISE:
|
||||
return tierEnterprise;
|
||||
default:
|
||||
return tierFree;
|
||||
return tierCustom;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,12 +30,12 @@ export type FeedbackOption = {
|
||||
export const feedbackOptions: FeedbackOption[] = [
|
||||
{
|
||||
type: 'general',
|
||||
desc: `Imagine evolves with your input. Share your thoughts and help us improve Imagine.`, // @todo fix
|
||||
desc: `${resolvedProfile.platform} evolves with your input. Share your thoughts and help us improve ${resolvedProfile.platform}.`,
|
||||
component: FeedbackGeneral
|
||||
},
|
||||
{
|
||||
type: 'nps',
|
||||
desc: `How likely are you to recommend Imagine to a friend or colleague?`, // @todo fix
|
||||
desc: `How likely are you to recommend ${resolvedProfile.platform} to a friend or colleague?`,
|
||||
component: FeedbackNps
|
||||
}
|
||||
];
|
||||
|
||||
@@ -102,13 +102,32 @@ export async function submitStripeCard(name: string, organizationId?: string) {
|
||||
}
|
||||
|
||||
if (setupIntent && setupIntent.status === 'succeeded') {
|
||||
if ((setupIntent.payment_method as PaymentMethod).card?.country === 'US') {
|
||||
const pm = setupIntent.payment_method as PaymentMethod | string | undefined;
|
||||
// If Stripe returned an expanded PaymentMethod object, check the card country.
|
||||
// If it returned a string id (common), `typeof pm === 'string'` and we skip this.
|
||||
if (typeof pm !== 'string' && pm?.card?.country === 'US') {
|
||||
// need to get state
|
||||
return setupIntent.payment_method as PaymentMethod;
|
||||
return pm as PaymentMethod;
|
||||
}
|
||||
|
||||
// The backend expects a provider method ID (string). Extract the id
|
||||
// whether Stripe returned the id string or an expanded object.
|
||||
let providerId: string | undefined;
|
||||
if (typeof pm === 'string') {
|
||||
providerId = pm;
|
||||
} else {
|
||||
providerId = (pm as PaymentMethod)?.id;
|
||||
}
|
||||
|
||||
if (!providerId) {
|
||||
const e = new Error('Unable to verify payment method.');
|
||||
trackError(e, Submit.PaymentMethodCreate);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const method = await sdk.forConsole.billing.setPaymentMethod(
|
||||
paymentMethod.$id,
|
||||
(setupIntent.payment_method as PaymentMethod).id,
|
||||
providerId,
|
||||
name
|
||||
);
|
||||
paymentElement.destroy();
|
||||
|
||||
@@ -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,31 +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 { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -40,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) {
|
||||
@@ -53,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="{resolvedProfile.platform} Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="120"
|
||||
class="u-block u-only-light"
|
||||
alt="{resolvedProfile.platform} 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';
|
||||
@@ -37,6 +37,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;
|
||||
@@ -93,6 +114,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">
|
||||
@@ -105,16 +129,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)}
|
||||
@@ -133,16 +160,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}
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
resolvedProfile.organizationPlatform,
|
||||
null,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
@@ -150,7 +150,7 @@
|
||||
selectedOrg.$id,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators
|
||||
);
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import { isSameDay } from '$lib/helpers/date';
|
||||
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
|
||||
import DbOperatorsDark from '$lib/images/promos/db-operators-dark.png';
|
||||
import DbOperatorsLight from '$lib/images/promos/db-operators-light.png';
|
||||
import AiSuggestionsDark from '$lib/images/promos/ai-suggestions-dark.png';
|
||||
import AiSuggestionsLight from '$lib/images/promos/ai-suggestions-light.png';
|
||||
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
|
||||
const listOfPromotions: BottomModalAlertItem[] = [];
|
||||
|
||||
if (isCloud) {
|
||||
const dbOperatorsPromo: BottomModalAlertItem = {
|
||||
id: 'modal:db_operators_announcement',
|
||||
const aiSuggestionsPromo: BottomModalAlertItem = {
|
||||
id: 'modal:database_ai_suggestions_announcement',
|
||||
src: {
|
||||
dark: DbOperatorsDark,
|
||||
light: DbOperatorsLight
|
||||
dark: AiSuggestionsDark,
|
||||
light: AiSuggestionsLight
|
||||
},
|
||||
title: 'Announcing DB operators',
|
||||
message: 'Update multiple fields without fetching the entire row.',
|
||||
title: 'Announcing Database AI suggestions',
|
||||
message: 'From table name to schema in one click.',
|
||||
plan: 'free',
|
||||
importance: 8,
|
||||
scope: 'project',
|
||||
cta: {
|
||||
text: 'Read announcement',
|
||||
link: () => 'https://appwrite.io/blog/post/announcing-db-operators',
|
||||
link: () => 'https://appwrite.io/blog/post/announcing-database-ai-suggestions',
|
||||
external: true,
|
||||
hideOnClick: true
|
||||
},
|
||||
show: true
|
||||
};
|
||||
listOfPromotions.push(dbOperatorsPromo);
|
||||
listOfPromotions.push(aiSuggestionsPromo);
|
||||
}
|
||||
|
||||
export function addBottomModalAlerts() {
|
||||
|
||||
@@ -115,8 +115,7 @@
|
||||
name,
|
||||
resolvedProfile.freeTier,
|
||||
null,
|
||||
resolvedProfile.organizationPlatform,
|
||||
null
|
||||
resolvedProfile.organizationPlatform
|
||||
);
|
||||
} else {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
@@ -125,7 +124,7 @@
|
||||
selectedPlan,
|
||||
paymentMethodId,
|
||||
resolvedProfile.organizationPlatform,
|
||||
null,
|
||||
undefined,
|
||||
selectedCoupon?.code,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
organizationName,
|
||||
resolvedProfile.freeTier,
|
||||
null,
|
||||
resolvedProfile.organizationPlatform,
|
||||
null
|
||||
resolvedProfile.organizationPlatform
|
||||
);
|
||||
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
|
||||
@@ -46,8 +46,7 @@ export const load: PageLoad = async ({ parent }) => {
|
||||
'Personal projects',
|
||||
resolvedProfile.freeTier,
|
||||
null,
|
||||
resolvedProfile.organizationPlatform,
|
||||
null
|
||||
resolvedProfile.organizationPlatform
|
||||
);
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: tierToPlan(BillingPlan.FREE)?.name,
|
||||
|
||||
@@ -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';
|
||||
@@ -8,7 +8,9 @@ import { isCloud } from '$lib/system';
|
||||
import { base } from '$app/paths';
|
||||
import { isFreePlan } from '$lib/helpers/billing';
|
||||
|
||||
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')) {
|
||||
@@ -20,6 +22,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
|
||||
@@ -35,9 +39,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
|
||||
@@ -85,6 +94,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)
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -152,7 +152,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
@@ -200,7 +200,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,
|
||||
@@ -142,30 +141,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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,8 +158,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
|
||||
@@ -256,7 +238,7 @@
|
||||
data.organization.$id,
|
||||
selectedPlan,
|
||||
paymentMethodId,
|
||||
null,
|
||||
undefined,
|
||||
selectedCoupon?.code,
|
||||
newCollaborators,
|
||||
billingBudget,
|
||||
|
||||
@@ -48,27 +48,29 @@ export const load: LayoutLoad = async ({ params, route, depends, parent }) => {
|
||||
// fast path without a network call!
|
||||
let organization = organizations?.teams?.find((org) => org.$id === project.teamId);
|
||||
|
||||
const includedInBasePlans = plansInfo.has(organization.billingPlan);
|
||||
// organization can be null if not in the filtered list!
|
||||
const includedInBasePlans = plansInfo.has(organization?.billingPlan);
|
||||
|
||||
const [org, regionalConsoleVariables, rolesResult, organizationPlan] = await Promise.all([
|
||||
const [org, regionalConsoleVariables, rolesResult] = await Promise.all([
|
||||
!organization
|
||||
? (sdk.forConsole.teams.get({ teamId: project.teamId }) as Promise<Organization>)
|
||||
: organization,
|
||||
sdk.forConsoleIn(project.region).console.variables(),
|
||||
isCloud ? sdk.forConsole.billing.getRoles(project.teamId) : null,
|
||||
|
||||
// fetch if not available in `plansInfo`
|
||||
includedInBasePlans
|
||||
? plansInfo.get(organization.billingPlan)
|
||||
: isCloud
|
||||
? sdk.forConsole.billing.getOrganizationPlan(organization.$id)
|
||||
: null,
|
||||
|
||||
loadAvailableRegions(project.teamId)
|
||||
]);
|
||||
|
||||
if (!organization) organization = org;
|
||||
|
||||
// fetch if not available in `plansInfo`.
|
||||
// out of promise.all because we filter orgs based on platform now!
|
||||
const organizationPlan = includedInBasePlans
|
||||
? plansInfo.get(organization?.billingPlan)
|
||||
: isCloud
|
||||
? await sdk.forConsole.billing.getOrganizationPlan(organization?.$id)
|
||||
: null;
|
||||
|
||||
const roles = rolesResult?.roles ?? defaultRoles;
|
||||
const scopes = rolesResult?.scopes ?? defaultScopes;
|
||||
|
||||
|
||||
+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
@@ -99,7 +99,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
@@ -16,7 +16,7 @@
|
||||
Enable or disable {resolvedProfile.platform} 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,14 +7,13 @@
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
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';
|
||||
import { Alert, Icon, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
import { isFreePlan } from '$lib/helpers/billing';
|
||||
|
||||
export let showCreate = false;
|
||||
let totalPolicies: UserBackupPolicy[] = [];
|
||||
@@ -132,7 +131,7 @@
|
||||
<CustomId bind:show={showCustomId} name="Database" bind:id autofocus={false} />
|
||||
|
||||
{#if isCloud}
|
||||
{#if isFreePlan($organization?.billingPlan)}
|
||||
{#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">
|
||||
|
||||
-5
@@ -82,9 +82,4 @@
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ai-icon-holder.notification) {
|
||||
width: 36px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
+5
-1
@@ -135,6 +135,7 @@ export type ColumnInput = {
|
||||
formatOptions?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
elements?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -155,7 +156,10 @@ export function mapSuggestedColumns<T extends ColumnInput>(columns: T[]): Sugges
|
||||
? (col.max ?? col.formatOptions?.max ?? undefined)
|
||||
: undefined,
|
||||
format: col.format ?? null,
|
||||
elements: col.elements ?? undefined
|
||||
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}
|
||||
|
||||
+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 }];
|
||||
}
|
||||
});
|
||||
|
||||
+25
@@ -251,6 +251,7 @@
|
||||
data-mode={mode}
|
||||
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
|
||||
@@ -381,6 +382,19 @@
|
||||
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;
|
||||
@@ -393,12 +407,16 @@
|
||||
}
|
||||
|
||||
&[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;
|
||||
}
|
||||
@@ -463,4 +481,11 @@
|
||||
margin-bottom: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
// global but controlled properly!
|
||||
:global(main:has(.no-custom-columns) .console-container) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+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';
|
||||
@@ -135,7 +135,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,
|
||||
@@ -180,7 +180,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
|
||||
});
|
||||
|
||||
|
||||
+26
-8
@@ -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({
|
||||
@@ -41,7 +51,15 @@ export const load: PageLoad = async ({ params, depends, url, route, parent }) =>
|
||||
'buildDuration',
|
||||
'status',
|
||||
'type',
|
||||
'resourceId'
|
||||
'resourceId',
|
||||
'providerRepositoryUrl',
|
||||
'providerRepositoryOwner',
|
||||
'providerRepositoryName',
|
||||
'providerBranchUrl',
|
||||
'providerBranch',
|
||||
'providerCommitMessage',
|
||||
'providerCommitHash',
|
||||
'providerCommitUrl'
|
||||
]),
|
||||
...parsedQueries.values()
|
||||
]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -26,17 +26,16 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { scopes as allScopes } from '$lib/constants';
|
||||
import { onMount } from 'svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { scopes as allScopes, cloudOnlyBackupScopes } from '$lib/constants';
|
||||
import { Accordion, Divider, Layout, Selector } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let scopes: string[];
|
||||
|
||||
const scopeCatalog = new Set(allScopes.map((s) => s.scope));
|
||||
|
||||
const filteredScopes = allScopes.filter((scope) => {
|
||||
const baseFilteredScopes = allScopes.filter((scope) => {
|
||||
const val = scope.scope;
|
||||
if (!val) return false;
|
||||
|
||||
@@ -44,6 +43,23 @@
|
||||
return !legacyPrefixes.some((prefix) => val.startsWith(prefix));
|
||||
});
|
||||
|
||||
// insert cloud-only scopes right after databases.write
|
||||
const databasesWriteIndex = baseFilteredScopes.findIndex((s) => s.scope === 'databases.write');
|
||||
const filteredScopes =
|
||||
isCloud && databasesWriteIndex !== -1
|
||||
? [
|
||||
...baseFilteredScopes.slice(0, databasesWriteIndex + 1),
|
||||
...cloudOnlyBackupScopes,
|
||||
...baseFilteredScopes.slice(databasesWriteIndex + 1)
|
||||
]
|
||||
: baseFilteredScopes;
|
||||
|
||||
// include all scopes
|
||||
const scopeCatalog = new Set([
|
||||
...allScopes.map((s) => s.scope),
|
||||
...(isCloud ? cloudOnlyBackupScopes.map((s) => s.scope) : [])
|
||||
]);
|
||||
|
||||
enum Category {
|
||||
Auth = 'Auth',
|
||||
Database = 'Database',
|
||||
|
||||
+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';
|
||||
@@ -28,6 +28,7 @@
|
||||
import { project } from '../../store';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import { getCorrectTitle, type PlatformProps } from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
|
||||
let { isConnectPlatform = false, platform = PlatformType.Flutterandroid }: PlatformProps =
|
||||
$props();
|
||||
@@ -38,6 +39,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
@@ -28,6 +28,7 @@
|
||||
import { project } from '../../store';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import { getCorrectTitle, type PlatformProps } from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
|
||||
let { isConnectPlatform = false, platform = PlatformType.Reactnativeandroid }: PlatformProps =
|
||||
$props();
|
||||
@@ -39,6 +40,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';
|
||||
|
||||
@@ -46,6 +69,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
|
||||
@@ -226,6 +255,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,8 +48,14 @@
|
||||
} from './components/index';
|
||||
import { extendedHostnameRegex } from '$lib/helpers/string';
|
||||
import { project } from '../../store';
|
||||
import {
|
||||
type PlatformProps,
|
||||
type FrameworkType,
|
||||
type LLMPromptConfig,
|
||||
getCorrectTitle
|
||||
} from './store';
|
||||
import LlmBanner from './llmBanner.svelte';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import { type PlatformProps, type FrameworkType, getCorrectTitle } from './store';
|
||||
|
||||
let { key, isConnectPlatform = false, platform = PlatformType.Web }: PlatformProps = $props();
|
||||
|
||||
@@ -158,6 +164,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';
|
||||
@@ -302,6 +359,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';
|
||||
@@ -173,7 +173,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
|
||||
});
|
||||
|
||||
|
||||
+4
-1
@@ -67,7 +67,10 @@
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="s" direction="row">
|
||||
<Button href="#" secondary>Docs</Button>
|
||||
<Button
|
||||
href="https://appwrite.io/docs/products/sites/deploy-from-git"
|
||||
external
|
||||
secondary>Docs</Button>
|
||||
<Button
|
||||
href={`https://github.com/${data.installations.installations[0].organization}`}
|
||||
text>Go to GitHub</Button>
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@
|
||||
import Details from '../../details.svelte';
|
||||
import Configuration from './configuration.svelte';
|
||||
import Aside from '../../aside.svelte';
|
||||
import { Adapter, BuildRuntime, Framework, ID, type Models } from '@appwrite.io/console';
|
||||
import { Adapter, BuildRuntime, Framework, ID, Type, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
ConnectBehaviour,
|
||||
NewRepository,
|
||||
@@ -160,7 +160,8 @@
|
||||
repository: data.template.providerRepositoryId,
|
||||
owner: data.template.providerOwner,
|
||||
rootDirectory: framework.providerRootDirectory,
|
||||
version: data.template.providerVersion,
|
||||
type: Type.Tag,
|
||||
reference: data.template.providerVersion,
|
||||
activate: true
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { RuleType } from '$lib/stores/sdk';
|
||||
import { DeploymentResourceType } from '$lib/stores/sdk';
|
||||
|
||||
@@ -15,7 +15,19 @@ export const load = async ({ params, depends, parent }) => {
|
||||
queries: [
|
||||
Query.limit(4),
|
||||
Query.orderDesc(''),
|
||||
Query.select(['status', 'type', 'resourceId'])
|
||||
Query.select([
|
||||
'status',
|
||||
'type',
|
||||
'resourceId',
|
||||
'providerRepositoryUrl',
|
||||
'providerRepositoryOwner',
|
||||
'providerRepositoryName',
|
||||
'providerBranchUrl',
|
||||
'providerBranch',
|
||||
'providerCommitMessage',
|
||||
'providerCommitHash',
|
||||
'providerCommitUrl'
|
||||
])
|
||||
]
|
||||
}),
|
||||
sdk.forProject(params.region, params.project).sites.listDeployments({
|
||||
@@ -46,11 +58,18 @@ export const load = async ({ params, depends, parent }) => {
|
||||
})
|
||||
]);
|
||||
|
||||
const deployment = deploymentList?.total
|
||||
? await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.sites.getDeployment({ siteId: params.site, deploymentId: site.deploymentId })
|
||||
: null;
|
||||
let deployment: Models.Deployment | null = null;
|
||||
if (deploymentList?.total && site.deploymentId) {
|
||||
try {
|
||||
deployment = await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.sites.getDeployment({ siteId: params.site, deploymentId: site.deploymentId });
|
||||
} catch (error) {
|
||||
// active deployment with the requested ID could not be found
|
||||
deployment = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
site,
|
||||
deploymentList,
|
||||
|
||||
+23
-8
@@ -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';
|
||||
@@ -29,7 +29,15 @@ export const load = async ({ params, depends, url, route, parent }) => {
|
||||
'buildDuration',
|
||||
'status',
|
||||
'type',
|
||||
'resourceId'
|
||||
'resourceId',
|
||||
'providerRepositoryUrl',
|
||||
'providerRepositoryOwner',
|
||||
'providerRepositoryName',
|
||||
'providerBranchUrl',
|
||||
'providerBranch',
|
||||
'providerCommitMessage',
|
||||
'providerCommitHash',
|
||||
'providerCommitUrl'
|
||||
]),
|
||||
...parsedQueries.values()
|
||||
]
|
||||
@@ -37,17 +45,24 @@ export const load = async ({ params, depends, url, route, parent }) => {
|
||||
sdk.forProject(params.region, params.project).vcs.listInstallations()
|
||||
]);
|
||||
|
||||
let activeDeployment: Models.Deployment | null = null;
|
||||
if (site.deploymentId && deploymentList?.total) {
|
||||
try {
|
||||
activeDeployment = await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.sites.getDeployment({ siteId: params.site, deploymentId: site.deploymentId });
|
||||
} catch (error) {
|
||||
// active deployment with the requested ID could not be found
|
||||
activeDeployment = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
query,
|
||||
deploymentList,
|
||||
activeDeployment:
|
||||
site.deploymentId && deploymentList?.total
|
||||
? await sdk
|
||||
.forProject(params.region, params.project)
|
||||
.sites.getDeployment({ siteId: params.site, deploymentId: site.deploymentId })
|
||||
: null,
|
||||
activeDeployment,
|
||||
installations
|
||||
};
|
||||
};
|
||||
|
||||
+38
-2
@@ -32,7 +32,8 @@
|
||||
allowedFileExtensions: values.allowedFileExtensions,
|
||||
compression: values.compression,
|
||||
encryption: values.encryption,
|
||||
antivirus: values.antivirus
|
||||
antivirus: values.antivirus,
|
||||
transformations: values.transformations
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.BUCKET);
|
||||
@@ -102,7 +103,8 @@
|
||||
$permissions: permissions,
|
||||
encryption,
|
||||
antivirus,
|
||||
compression
|
||||
compression,
|
||||
transformations
|
||||
} = data.bucket;
|
||||
|
||||
const compressionOptions = [
|
||||
@@ -215,6 +217,18 @@
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function updateTransformations() {
|
||||
updateBucket(
|
||||
data.bucket,
|
||||
{
|
||||
transformations
|
||||
},
|
||||
{
|
||||
trackEventName: Submit.BucketUpdateTransformations
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -366,6 +380,28 @@
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<Form onSubmit={updateTransformations}>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Image transformations</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<Selector.Switch
|
||||
label="Image transformations"
|
||||
id="transformations"
|
||||
bind:checked={transformations}
|
||||
description="Enabling this option allows image manipulation through the API, including resizing, cropping, and format conversion." />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
disabled={transformations === data.bucket.transformations ||
|
||||
($readOnly && !GRACE_PERIOD_OVERRIDE)}
|
||||
submit>
|
||||
Update
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<UpdateMaxFileSize currentPlan={data.currentPlan} bucket={data.bucket} />
|
||||
|
||||
<Form onSubmit={updateAllowedExtensions}>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { Icon, Layout, Tag, Typography, Button, Card } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Input, Layout, Popover, Tag, Typography, Card } from '@appwrite.io/pink-svelte';
|
||||
import { supportData, isSupportOnline } from './wizard/support/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Form, InputSelect, InputText, InputTextarea } from '$lib/elements/forms/index.js';
|
||||
|
||||
import {
|
||||
Form,
|
||||
InputSelect,
|
||||
InputText,
|
||||
InputTextarea,
|
||||
Button
|
||||
} from '$lib/elements/forms/index.js';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import {
|
||||
localeTimezoneName,
|
||||
@@ -18,30 +24,99 @@
|
||||
import { user } from '$lib/stores/user';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { VARS } from '$lib/system';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { IconCheckCircle, IconXCircle } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconCheckCircle, IconXCircle, IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
|
||||
let projectOptions: Array<{ value: string; label: string }>;
|
||||
let projectOptions = $state<Array<{ value: string; label: string }>>([]);
|
||||
|
||||
// Category options with display names
|
||||
const categories = [
|
||||
{ value: 'general', label: 'General' },
|
||||
{ value: 'billing', label: 'Billing' },
|
||||
{ value: 'technical', label: 'Technical' }
|
||||
];
|
||||
|
||||
// Topic options based on category
|
||||
const topicsByCategory = {
|
||||
general: [
|
||||
'Security',
|
||||
'Compliance',
|
||||
'Performance',
|
||||
'Account',
|
||||
'Project',
|
||||
'Regions',
|
||||
'Other'
|
||||
],
|
||||
billing: ['Invoice', 'Plans', 'Payment methods', 'Downgrade', 'Refund', 'Usage', 'Other'],
|
||||
technical: [
|
||||
'Auth',
|
||||
'Databases',
|
||||
'Storage',
|
||||
'Functions',
|
||||
'Realtime',
|
||||
'Messaging',
|
||||
'Migrations',
|
||||
'Webhooks',
|
||||
'SDKs',
|
||||
'Console',
|
||||
'Backups',
|
||||
'Blocked project',
|
||||
'Domains',
|
||||
'Outage',
|
||||
'Platforms',
|
||||
'Sites',
|
||||
'Other'
|
||||
]
|
||||
};
|
||||
|
||||
// Severity options
|
||||
const severityOptions = [
|
||||
{ value: 'critical', label: 'Critical' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'question', label: 'Question' }
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const projectList = await sdk.forConsole.projects.list();
|
||||
// Filter projects by organization ID using server-side queries
|
||||
const projectList = await sdk.forConsole.projects.list({
|
||||
queries: $organization?.$id ? [Query.equal('teamId', $organization.$id)] : []
|
||||
});
|
||||
projectOptions = projectList.projects.map((project) => ({
|
||||
value: project.$id,
|
||||
label: project.name
|
||||
}));
|
||||
});
|
||||
|
||||
// Cleanup on component destroy
|
||||
onDestroy(() => {
|
||||
$supportData = {
|
||||
message: null,
|
||||
subject: null,
|
||||
category: 'general',
|
||||
category: 'technical',
|
||||
topic: undefined,
|
||||
severity: 'question',
|
||||
file: null
|
||||
};
|
||||
});
|
||||
|
||||
// Update topic options when category changes
|
||||
const topicOptions = $derived(
|
||||
($supportData.category ? topicsByCategory[$supportData.category] || [] : []).map(
|
||||
(topic) => ({
|
||||
value: topic.toLowerCase().trim().replace(/\s+/g, '-'),
|
||||
label: topic
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
async function handleSubmit() {
|
||||
// Create category-topic tag
|
||||
const categoryTopicTag = $supportData.topic
|
||||
? `${$supportData.category}-${$supportData.topic}`.toLowerCase()
|
||||
: $supportData.category.toLowerCase();
|
||||
|
||||
const response = await fetch(`${VARS.GROWTH_ENDPOINT}/support`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -52,13 +127,13 @@
|
||||
subject: $supportData.subject,
|
||||
firstName: ($user?.name || 'Unknown').slice(0, 40),
|
||||
message: $supportData.message,
|
||||
tags: ['cloud'],
|
||||
tags: [categoryTopicTag],
|
||||
customFields: [
|
||||
{ id: '41612', value: $supportData.category },
|
||||
{ id: '48493', value: $user?.name ?? '' },
|
||||
{ id: '48492', value: $organization?.$id ?? '' },
|
||||
{ id: '48491', value: $supportData?.project ?? '' },
|
||||
{ id: '48490', value: $user?.$id ?? '' }
|
||||
{ id: '56023', value: $supportData?.severity ?? '' },
|
||||
{ id: '56024', value: $organization?.billingPlan ?? '' }
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -84,7 +159,9 @@
|
||||
$supportData = {
|
||||
message: null,
|
||||
subject: null,
|
||||
category: 'general',
|
||||
category: 'technical',
|
||||
topic: undefined,
|
||||
severity: undefined,
|
||||
file: null,
|
||||
project: null
|
||||
};
|
||||
@@ -99,10 +176,43 @@
|
||||
endDay: 'Friday' as WeekDay
|
||||
};
|
||||
|
||||
$: supportTimings = `${utcHourToLocaleHour(workTimings.start)} - ${utcHourToLocaleHour(workTimings.end)} ${localeTimezoneName()}`;
|
||||
$: supportWeekDays = `${utcWeekDayToLocaleWeekDay(workTimings.startDay, workTimings.start)} - ${utcWeekDayToLocaleWeekDay(workTimings.endDay, workTimings.end)}`;
|
||||
const supportTimings = $derived(
|
||||
`${utcHourToLocaleHour(workTimings.start)} - ${utcHourToLocaleHour(workTimings.end)} ${localeTimezoneName()}`
|
||||
);
|
||||
const supportWeekDays = $derived(
|
||||
`${utcWeekDayToLocaleWeekDay(workTimings.startDay, workTimings.start)} - ${utcWeekDayToLocaleWeekDay(workTimings.endDay, workTimings.end)}`
|
||||
);
|
||||
</script>
|
||||
|
||||
{#snippet severityPopover()}
|
||||
<Popover let:toggle>
|
||||
<Button extraCompact size="s" on:click={toggle}>
|
||||
<Icon size="s" icon={IconInfo} />
|
||||
</Button>
|
||||
<div slot="tooltip" style="max-width: 400px;">
|
||||
<Layout.Stack gap="s">
|
||||
<Typography.Text>
|
||||
<b>Critical:</b> System is down or a critical component is non-functional, causing
|
||||
a complete stoppage of work or significant business impact.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>High:</b> Major functionality is impaired, but a workaround is available, or a
|
||||
critical component is significantly degraded.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>Medium:</b> Minor functionality is impaired without significant business impact.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>Low:</b> Issue has minor impact on business operations; workaround is not necessary.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>Question:</b> Requests for information, general guidance, or feature requests.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Popover>
|
||||
{/snippet}
|
||||
|
||||
<Wizard title="Contact us" confirmExit={true}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Layout.Stack gap="xl">
|
||||
@@ -113,24 +223,48 @@
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="s">
|
||||
<Typography.Text color="--fgcolor-neutral-secondary"
|
||||
>Choose a topic</Typography.Text>
|
||||
>Choose a category</Typography.Text>
|
||||
<Layout.Stack gap="s" direction="row">
|
||||
{#each ['general', 'billing', 'technical'] as category}
|
||||
{#each categories as category}
|
||||
<Tag
|
||||
on:click={() => {
|
||||
$supportData.category = category;
|
||||
if ($supportData.category !== category.value) {
|
||||
$supportData.topic = undefined;
|
||||
}
|
||||
$supportData.category = category.value;
|
||||
}}
|
||||
selected={$supportData.category === category}>{category}</Tag>
|
||||
selected={$supportData.category === category.value}
|
||||
>{category.label}</Tag>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<InputSelect
|
||||
required
|
||||
{#if topicOptions.length > 0}
|
||||
{#key $supportData.category}
|
||||
<Input.ComboBox
|
||||
id="topic"
|
||||
label="Choose a topic"
|
||||
placeholder="Select topic"
|
||||
bind:value={$supportData.topic}
|
||||
options={topicOptions} />
|
||||
{/key}
|
||||
{/if}
|
||||
<Input.ComboBox
|
||||
id="project"
|
||||
label="Choose a project"
|
||||
options={projectOptions ?? []}
|
||||
bind:value={$supportData.project}
|
||||
placeholder="Select project" />
|
||||
<InputSelect
|
||||
id="severity"
|
||||
label="Severity"
|
||||
options={severityOptions}
|
||||
bind:value={$supportData.severity}
|
||||
required
|
||||
placeholder="Select severity">
|
||||
<div slot="info">
|
||||
{@render severityPopover()}
|
||||
</div>
|
||||
</InputSelect>
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
@@ -143,15 +277,16 @@
|
||||
bind:value={$supportData.message}
|
||||
placeholder="Type here..."
|
||||
label="Tell us a bit more"
|
||||
required
|
||||
maxlength={4096} />
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" gap="s">
|
||||
<Button.Button
|
||||
<Button
|
||||
size="s"
|
||||
variant="secondary"
|
||||
secondary
|
||||
on:click={() => {
|
||||
wizard.hide();
|
||||
}}>Cancel</Button.Button>
|
||||
<Button.Button size="s">Submit</Button.Button>
|
||||
}}>Cancel</Button>
|
||||
<Button submit size="s">Submit</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
|
||||
@@ -4,6 +4,8 @@ export type SupportData = {
|
||||
message: string;
|
||||
subject: string;
|
||||
category: string;
|
||||
topic?: string;
|
||||
severity?: string;
|
||||
file?: File | null;
|
||||
project?: string;
|
||||
};
|
||||
@@ -11,7 +13,8 @@ export type SupportData = {
|
||||
export const supportData = writable<SupportData>({
|
||||
message: '',
|
||||
subject: '',
|
||||
category: 'general',
|
||||
category: 'technical',
|
||||
severity: 'question',
|
||||
file: null
|
||||
});
|
||||
|
||||
|
||||
@@ -81,8 +81,7 @@ export const load: PageLoad = async ({ parent, url }) => {
|
||||
'Personal Projects',
|
||||
resolvedProfile.freeTier,
|
||||
null,
|
||||
resolvedProfile.organizationPlatform,
|
||||
null
|
||||
resolvedProfile.organizationPlatform
|
||||
);
|
||||
} else {
|
||||
await sdk.forConsole.teams.create({
|
||||
|
||||
@@ -55,7 +55,7 @@ export const load = async ({ parent, url, params }) => {
|
||||
'Personal project',
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
resolvedProfile.organizationPlatform
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -363,6 +363,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix when no vertical scrollbar is present, some environments reserve a gutter by default */
|
||||
html {
|
||||
scrollbar-gutter: auto !important;
|
||||
}
|
||||
|
||||
/* TODO: remove this block once Pink V2 is incorporated */
|
||||
input[type='radio'],
|
||||
input[type='checkbox']:not([class='switch']),
|
||||
|
||||
Reference in New Issue
Block a user