mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'feat-pink-v2' into remove-jwt
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Avatar } from '@appwrite.io/pink-svelte';
|
||||
import type { AvatarProps } from '@appwrite.io/pink-svelte/dist/avatar/Avatar.svelte';
|
||||
|
||||
type AvatarProps = Partial<{
|
||||
src: string;
|
||||
alt: string;
|
||||
size: 'xs' | 's' | 'm' | 'l' | 'xl';
|
||||
empty: boolean;
|
||||
}>;
|
||||
|
||||
export let size: AvatarProps['size'] = 'm';
|
||||
export let src: AvatarProps['src'] = undefined;
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
tooltipShow={plan.$id === BillingPlan.FREE && anyOrgFree}
|
||||
tooltipText={plan.$id === BillingPlan.FREE
|
||||
? 'You are limited to 1 Free organization per account.'
|
||||
: ''}
|
||||
padding={1.5}>
|
||||
: ''}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import {
|
||||
type BottomModalAlertItem,
|
||||
bottomModalAlerts,
|
||||
bottomModalAlertsConfig,
|
||||
dismissBottomModalAlert,
|
||||
hideAllModalAlerts
|
||||
} from '$lib/stores/bottom-alerts';
|
||||
@@ -16,6 +16,7 @@
|
||||
import { project } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { page } from '$app/state';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let currentIndex = 0;
|
||||
let openModalOnMobile = false;
|
||||
@@ -33,23 +34,27 @@
|
||||
return alerts
|
||||
.sort((a, b) => b.importance - a.importance)
|
||||
.filter((alert) => {
|
||||
return (
|
||||
alert.show &&
|
||||
shouldShowNotification(alert.id) &&
|
||||
// if no scope > show in projects & org pages.
|
||||
((!alert.scope && (isProjectPage || isOrganizationPage)) ||
|
||||
// project scope, show only in project pages
|
||||
(isProjectPage && alert.scope === 'project') ||
|
||||
// organization scope, show only in organization pages
|
||||
(isOrganizationPage && alert.scope === 'organization'))
|
||||
);
|
||||
if (!alert.show || !shouldShowNotification(alert.id)) return false;
|
||||
|
||||
switch (alert.scope) {
|
||||
case 'everywhere':
|
||||
return true;
|
||||
case 'project':
|
||||
return isProjectPage;
|
||||
case 'organization':
|
||||
return isOrganizationPage;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$: filteredModalAlerts = filterModalAlerts($bottomModalAlerts, page.route.id);
|
||||
$: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, page.route.id);
|
||||
|
||||
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
|
||||
|
||||
$: hasOnlyPrimaryCta = typeof currentModalAlert?.learnMore === 'undefined';
|
||||
|
||||
function handleClose() {
|
||||
filteredModalAlerts.forEach((alert) => {
|
||||
const modalAlert = alert;
|
||||
@@ -67,9 +72,48 @@
|
||||
currentIndex = (currentIndex - 1 + filteredModalAlerts.length) % filteredModalAlerts.length;
|
||||
}
|
||||
|
||||
function getMobileWindowConfig(): {
|
||||
html: boolean;
|
||||
cta: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
} {
|
||||
const config = $bottomModalAlertsConfig?.mobileSingleLayout;
|
||||
const visibleAlerts = $bottomModalAlertsConfig.alerts.filter((a) => a.show);
|
||||
|
||||
const fallback = {
|
||||
title: 'New features available',
|
||||
message: 'Explore new features to enhance your projects and improve security.'
|
||||
};
|
||||
|
||||
const shouldApplyConfig = config?.enabled === true && visibleAlerts.length === 1;
|
||||
|
||||
return {
|
||||
cta: !!(shouldApplyConfig && config.cta),
|
||||
html: !!(shouldApplyConfig && config.isHtml),
|
||||
title: shouldApplyConfig && config.title ? config.title : fallback.title,
|
||||
message: shouldApplyConfig && config.message ? config.message : fallback.message
|
||||
};
|
||||
}
|
||||
|
||||
function triggerMobileWindowLink() {
|
||||
handleClose();
|
||||
|
||||
const url = $bottomModalAlertsConfig.mobileSingleLayout.cta.link({
|
||||
organization: $organization,
|
||||
project: $project
|
||||
});
|
||||
|
||||
if ($bottomModalAlertsConfig.mobileSingleLayout.cta.external) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
goto(url);
|
||||
}
|
||||
}
|
||||
|
||||
function showUpgrade() {
|
||||
const plan = currentModalAlert.plan;
|
||||
const organizationPlan = $organization.billingPlan;
|
||||
const organizationPlan = $organization?.billingPlan;
|
||||
switch (plan) {
|
||||
case 'free':
|
||||
return false;
|
||||
@@ -87,7 +131,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert}
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert && !page.url.pathname.includes('console/onboarding')}
|
||||
{@const shouldShowUpgrade = showUpgrade()}
|
||||
<div class="main-alert-wrapper is-not-mobile">
|
||||
<div class="alert-container">
|
||||
@@ -164,8 +208,8 @@
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
class="button"
|
||||
secondary={!hasOnlyPrimaryCta}
|
||||
class={`${hasOnlyPrimaryCta ? 'only-primary-cta' : ''}`}
|
||||
href={shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
: currentModalAlert.cta.link({
|
||||
@@ -175,6 +219,12 @@
|
||||
external={!!currentModalAlert.cta.external}
|
||||
fullWidthMobile
|
||||
on:click={() => {
|
||||
if (currentModalAlert.cta?.hideOnClick === true) {
|
||||
// be careful of this one.
|
||||
// once clicked, its gone!
|
||||
handleClose();
|
||||
}
|
||||
|
||||
trackEvent(Click.PromoClick, {
|
||||
promo: currentModalAlert.id,
|
||||
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
|
||||
@@ -280,7 +330,7 @@
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
secondary={!hasOnlyPrimaryCta}
|
||||
class="button"
|
||||
href={shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
@@ -322,20 +372,35 @@
|
||||
</article>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
{@const mobileConfig = getMobileWindowConfig()}
|
||||
<div
|
||||
class:showing={!openModalOnMobile}
|
||||
class="card notification-card u-width-full-line"
|
||||
on:click={() => (openModalOnMobile = true)}>
|
||||
on:click={() => {
|
||||
if (mobileConfig.cta) {
|
||||
// navigate manually!
|
||||
triggerMobileWindowLink();
|
||||
} else {
|
||||
openModalOnMobile = true;
|
||||
}
|
||||
}}>
|
||||
<div class="u-flex-vertical u-gap-4">
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<h3 class="body-text-2 u-bold">New features available</h3>
|
||||
<h3 class="body-text-2 u-bold">{mobileConfig.title}</h3>
|
||||
<button on:click={hideAllModalAlerts} aria-label="Close">
|
||||
<span class="icon-x"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
Explore new features to enhance your projects and improve security.
|
||||
{#if mobileConfig.html}
|
||||
{@html mobileConfig.message}
|
||||
{:else}
|
||||
{mobileConfig.message}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -377,6 +442,22 @@
|
||||
border: hsl(var(--color-neutral-80)) solid 1px;
|
||||
}
|
||||
|
||||
:global(.main-alert-wrapper .only-primary-cta) {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.showcase-image.u-only-light {
|
||||
border-radius: 8px;
|
||||
border: 0.795px solid var(--border-neutral-strong, #d8d8db);
|
||||
}
|
||||
|
||||
.showcase-image.u-only-dark {
|
||||
border-radius: 8px;
|
||||
border: 0.795px solid var(--border-neutral-strong, #414146);
|
||||
}
|
||||
|
||||
.u-gap-10 {
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '@appwrite.io/pink-svelte';
|
||||
import type Base from '@appwrite.io/pink-svelte/dist/card/Base.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import type { BaseCardProps } from './card.svelte';
|
||||
|
||||
export let radius: ComponentProps<Base>['radius'] = 'm';
|
||||
export let padding: ComponentProps<Base>['padding'] = 's';
|
||||
export let radius: BaseCardProps['radius'] = 'm';
|
||||
export let padding: BaseCardProps['padding'] = 's';
|
||||
</script>
|
||||
|
||||
<Card.Base variant="secondary" {radius} {padding}>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
name: string;
|
||||
$id: string;
|
||||
isSelected: boolean;
|
||||
region: string;
|
||||
};
|
||||
type Organization = {
|
||||
name: string;
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<script context="module" lang="ts">
|
||||
export type BaseCardProps = Partial<{
|
||||
variant: 'primary' | 'secondary';
|
||||
radius: 's' | 'm' | 'l';
|
||||
padding: 'none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l';
|
||||
border: 'solid' | 'dashed';
|
||||
shadow?: boolean;
|
||||
disabled?: boolean;
|
||||
}>;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Card, Layout } from '@appwrite.io/pink-svelte';
|
||||
import type Base from '@appwrite.io/pink-svelte/dist/card/Base.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
type BaseProps = {
|
||||
isDashed?: boolean;
|
||||
@@ -20,7 +29,7 @@
|
||||
isButton?: never;
|
||||
};
|
||||
|
||||
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & ComponentProps<Base>;
|
||||
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & BaseCardProps;
|
||||
|
||||
export let isDashed = false;
|
||||
export let isButton = false;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { Badge, Layout, Link, Popover, Table } from '@appwrite.io/pink-svelte';
|
||||
import CreditCardBrandImage from './creditCardBrandImage.svelte';
|
||||
import type { RootProp } from '@appwrite.io/pink-svelte/dist/table';
|
||||
import type { TableRootProp } from '$lib/helpers/types';
|
||||
|
||||
export let root: RootProp;
|
||||
export let root: TableRootProp;
|
||||
export let paymentMethod: PaymentMethodData;
|
||||
export let isBackup: boolean = false;
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Card, Tooltip, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { type ComponentProps } from 'svelte';
|
||||
import type Selector from '@appwrite.io/pink-svelte/dist/card/Selector.svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { BaseCardProps } from './card.svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
type Props = ComponentProps<Selector>;
|
||||
type Props = BaseCardProps &
|
||||
HTMLAttributes<HTMLInputElement> & {
|
||||
name: string;
|
||||
value: string;
|
||||
group: string;
|
||||
title: string;
|
||||
info?: string | undefined;
|
||||
icon?: ComponentType;
|
||||
imageHeight?: number;
|
||||
imageWidth?: number;
|
||||
imageRadius?: 'xxs' | 'xs' | 's' | 'm' | 'l';
|
||||
disabled?: boolean;
|
||||
src?: string;
|
||||
alt?: string | undefined;
|
||||
};
|
||||
|
||||
export let group: string;
|
||||
export let value: string;
|
||||
|
||||
@@ -2,10 +2,20 @@
|
||||
export type NavbarProject = {
|
||||
name: string;
|
||||
$id: string;
|
||||
region: string;
|
||||
isSelected: boolean;
|
||||
platformCount: number;
|
||||
pingCount: number;
|
||||
};
|
||||
|
||||
export type BaseNavbarProps = HTMLAttributes<HTMLHeadElement> & {
|
||||
logo: {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
avatar: string;
|
||||
sideBarIsOpen: boolean;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -23,7 +33,6 @@
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { toggleCommandCenter } from '$lib/commandCenter/commandCenter.svelte';
|
||||
import type { BaseNavbarProps } from '@appwrite.io/pink-svelte/dist/navbar/Base.svelte';
|
||||
import {
|
||||
IconChevronRight,
|
||||
IconLogoutRight,
|
||||
@@ -45,6 +54,7 @@
|
||||
import { isCloud } from '$lib/system.js';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let showSupport = false;
|
||||
|
||||
@@ -125,7 +135,7 @@
|
||||
{#if selectedProject && selectedProject.pingCount === 0}
|
||||
<div class="only-desktop" style:margin-inline-start="-16px">
|
||||
<Button.Anchor
|
||||
href={`${base}/project-${selectedProject.$id}/get-started`}
|
||||
href={`${base}/project-${selectedProject.region}-${selectedProject.$id}/get-started`}
|
||||
variant="secondary"
|
||||
size="xs">Connect</Button.Anchor>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,13 @@
|
||||
</script>
|
||||
|
||||
{#if !hidePages}
|
||||
<Pagination {limit} page={currentPage} {total} type="button" on:page={handleOptionClick} />
|
||||
<Pagination
|
||||
{limit}
|
||||
page={currentPage}
|
||||
{total}
|
||||
type="button"
|
||||
on:page={handleOptionClick}
|
||||
createLink={undefined as never} />
|
||||
{:else}
|
||||
<Layout.Stack direction="row" inline>
|
||||
<Button.Button
|
||||
|
||||
@@ -39,10 +39,11 @@
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { NavbarProject } from '$lib/components/navbar.svelte';
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLElement> & {
|
||||
state?: 'closed' | 'open' | 'icons';
|
||||
project: { $id: string } | undefined;
|
||||
project: NavbarProject | undefined;
|
||||
avatar: string;
|
||||
progressCard?: {
|
||||
title: string;
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
import { getElementDir } from '$lib/helpers/style';
|
||||
import { waitUntil } from '$lib/helpers/waitUntil';
|
||||
import { Tabs } from '@appwrite.io/pink-svelte';
|
||||
import type { Variant } from '@appwrite.io/pink-svelte/dist/tabs/types';
|
||||
|
||||
export let selected = false;
|
||||
export let href: string = null;
|
||||
export let event: string = null;
|
||||
export let noscroll = false;
|
||||
export let root: { variant: Variant; stretch: boolean } = {
|
||||
export let root: { variant: 'primary' | 'secondary'; stretch: boolean } = {
|
||||
variant: 'primary',
|
||||
stretch: false
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Input, Layout, Selector } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
export let label: string = '';
|
||||
export let value: string;
|
||||
export let required = false;
|
||||
export let nullable = false;
|
||||
@@ -48,7 +48,7 @@
|
||||
{value}
|
||||
{step}
|
||||
helper={error}
|
||||
on:change={(event) => (value = event.target.value)}
|
||||
on:change={(event) => (value = (event.target as HTMLInputElement).value)}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}>
|
||||
{#if nullable}
|
||||
<Selector.Checkbox
|
||||
|
||||
@@ -52,3 +52,16 @@ export function isValueOfStringEnum<T extends Record<string, string>>(
|
||||
): value is T[keyof T] {
|
||||
return Object.values<string>(enumType).includes(value);
|
||||
}
|
||||
|
||||
export type TableRootProp = {
|
||||
allowSelection: boolean;
|
||||
selectedRows: string[];
|
||||
selectedAll: boolean;
|
||||
selectedNone: boolean;
|
||||
selectedSome: boolean;
|
||||
columns: Record<PinkColumn['id'], PinkColumn>;
|
||||
toggle: (id: string) => void;
|
||||
toggleAll: () => void;
|
||||
addAvailableId: (id: string) => void;
|
||||
removeAvailableId: (id: string) => void;
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 368 KiB |
@@ -5,13 +5,13 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Layout, Table, Card, Empty } from '@appwrite.io/pink-svelte';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import type { Column } from '@appwrite.io/pink-svelte/dist/table';
|
||||
import type { PinkColumn } from '$lib/helpers/types';
|
||||
|
||||
export let logs: Models.LogList;
|
||||
export let offset = 0;
|
||||
export let limit = 0;
|
||||
|
||||
const columns: Column[] = [
|
||||
const columns: PinkColumn[] = [
|
||||
{ id: 'user' },
|
||||
{ id: 'event' },
|
||||
{ id: 'location' },
|
||||
|
||||
@@ -5,10 +5,28 @@ import type { Models } from '@appwrite.io/console';
|
||||
|
||||
type BottomModalAlertAction = {
|
||||
text: string;
|
||||
hideOnClick?: boolean;
|
||||
link: (ctx: { organization: Organization; project: Models.Project }) => string;
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Special layout settings for mobile when exactly one alert is shown.
|
||||
*
|
||||
* Applied only if `enabled` is `true` and the number of visible alerts is exactly 1.
|
||||
*
|
||||
* Useful when you want to display the alert directly in the floating window,
|
||||
* without opening a separate modal.
|
||||
*/
|
||||
export type MobileSingleAlertLayoutConfig = {
|
||||
title: string;
|
||||
message: string;
|
||||
enabled: boolean;
|
||||
isHtml?: boolean;
|
||||
// because message is the text!
|
||||
cta: Omit<BottomModalAlertAction, 'text'>;
|
||||
};
|
||||
|
||||
export type BottomModalAlertItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -23,18 +41,36 @@ export type BottomModalAlertItem = {
|
||||
importance?: number;
|
||||
|
||||
closed?: () => void;
|
||||
scope?: 'organization' | 'project';
|
||||
scope: 'organization' | 'project' | 'everywhere';
|
||||
notificationHideOptions?: NotificationCoolOffOptions;
|
||||
};
|
||||
|
||||
export const bottomModalAlerts = writable<BottomModalAlertItem[]>([]);
|
||||
type BottomModalAlertState = {
|
||||
alerts: BottomModalAlertItem[];
|
||||
mobileSingleLayout?: MobileSingleAlertLayoutConfig;
|
||||
};
|
||||
|
||||
export const bottomModalAlertsConfig = writable<BottomModalAlertState>({ alerts: [] });
|
||||
|
||||
export const hideAllModalAlerts = () => {
|
||||
bottomModalAlerts.update((all) => all.map((t) => ({ ...t, show: false })));
|
||||
bottomModalAlertsConfig.update((state) => ({
|
||||
...state,
|
||||
alerts: state.alerts.map((t) => ({ ...t, show: false }))
|
||||
}));
|
||||
};
|
||||
|
||||
export const setMobileSingleAlertLayout = (config: MobileSingleAlertLayoutConfig) => {
|
||||
bottomModalAlertsConfig.update((state) => ({
|
||||
...state,
|
||||
mobileSingleLayout: config
|
||||
}));
|
||||
};
|
||||
|
||||
export const dismissBottomModalAlert = (id: string) => {
|
||||
bottomModalAlerts.update((all) => all.filter((t) => t.id !== id));
|
||||
bottomModalAlertsConfig.update((state) => ({
|
||||
...state,
|
||||
alerts: state.alerts.filter((t) => t.id !== id)
|
||||
}));
|
||||
};
|
||||
|
||||
export const showBottomModalAlert = (notification: BottomModalAlertItem) => {
|
||||
@@ -45,8 +81,11 @@ export const showBottomModalAlert = (notification: BottomModalAlertItem) => {
|
||||
...notification
|
||||
};
|
||||
|
||||
bottomModalAlerts.update((all) => {
|
||||
if (all.some((t) => t.id === notification.id)) return all;
|
||||
return [...all, defaults as BottomModalAlertItem];
|
||||
bottomModalAlertsConfig.update((state) => {
|
||||
if (state.alerts.some((t) => t.id === notification.id)) return state;
|
||||
return {
|
||||
...state,
|
||||
alerts: [...state.alerts, defaults as BottomModalAlertItem]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
return {
|
||||
name: project?.name,
|
||||
$id: project.$id,
|
||||
region: project.region,
|
||||
isSelected: data.currentProjectId === project.$id,
|
||||
platformCount: project.platforms.length,
|
||||
pingCount: project.pingCount
|
||||
|
||||
@@ -53,6 +53,11 @@ export const load: LayoutLoad = async ({ params, fetch, depends, parent }) => {
|
||||
projects = orgProjects.projects.length > 0 ? orgProjects.projects : [];
|
||||
}
|
||||
|
||||
// set `default` if no region!
|
||||
for (const project of projects) {
|
||||
project.region ??= 'default';
|
||||
}
|
||||
|
||||
return {
|
||||
consoleVariables: variables,
|
||||
version: data?.version ?? null,
|
||||
|
||||
@@ -1,32 +1,46 @@
|
||||
import { base } from '$app/paths';
|
||||
import BackupsDark from '$lib/images/backups/promo/backups-dark.png';
|
||||
import BackupsLight from '$lib/images/backups/promo/backups-light.png';
|
||||
import Init3Promo from '$lib/images/promo-init3.png';
|
||||
|
||||
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
|
||||
import {
|
||||
type BottomModalAlertItem,
|
||||
setMobileSingleAlertLayout,
|
||||
showBottomModalAlert
|
||||
} from '$lib/stores/bottom-alerts';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
const listOfPromotions: BottomModalAlertItem[] = [];
|
||||
|
||||
if (isCloud) {
|
||||
const title = 'Join Init 19-23 May';
|
||||
const message =
|
||||
'This release will change the way you build with Appwrite forever. Register for Init and join the giveaway.';
|
||||
|
||||
const callToAction = {
|
||||
external: true,
|
||||
hideOnClick: true,
|
||||
text: 'Claim your ticket',
|
||||
link: () => 'https://apwr.dev/clcon'
|
||||
};
|
||||
|
||||
listOfPromotions.push({
|
||||
id: 'modal:databaseBackups',
|
||||
id: 'modal:init3',
|
||||
src: {
|
||||
dark: BackupsDark,
|
||||
light: BackupsLight
|
||||
dark: Init3Promo,
|
||||
light: Init3Promo
|
||||
},
|
||||
title: 'Database Backups are available now',
|
||||
message: 'Protect your data and ensure quick recovery with our new backups',
|
||||
plan: 'pro',
|
||||
scope: 'project',
|
||||
title,
|
||||
message,
|
||||
plan: 'free',
|
||||
importance: 8,
|
||||
cta: {
|
||||
text: 'Try now',
|
||||
link: ({ project }) => `${base}/project-${project.region}-${project.$id}/databases`
|
||||
},
|
||||
learnMore: {
|
||||
text: 'Learn more',
|
||||
link: () => 'https://appwrite.io/docs/products/databases/backups'
|
||||
}
|
||||
scope: 'everywhere',
|
||||
cta: callToAction
|
||||
});
|
||||
|
||||
// there's only one item.
|
||||
setMobileSingleAlertLayout({
|
||||
title,
|
||||
message,
|
||||
enabled: true,
|
||||
cta: callToAction
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,54 +2,27 @@
|
||||
import { Card, Layout, Button } from '@appwrite.io/pink-svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ID, Region, type Models } from '@appwrite.io/console';
|
||||
import { ID, Region } from '@appwrite.io/console';
|
||||
import Loading from './loading.svelte';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import CreateProject from '$lib/layout/createProject.svelte';
|
||||
|
||||
let isLoading = false;
|
||||
let id: string;
|
||||
let startAnimation = false;
|
||||
let projectName = 'Appwrite project';
|
||||
let region = Region.Default;
|
||||
export let data: { regions: Models.ConsoleRegionList | null };
|
||||
let region = Region.Fra;
|
||||
export let data;
|
||||
|
||||
async function createProject() {
|
||||
isLoading = true;
|
||||
|
||||
let org;
|
||||
try {
|
||||
org = isCloud
|
||||
? await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal projects',
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
)
|
||||
: await sdk.forConsole.teams.create(ID.unique(), 'Personal projects');
|
||||
} catch (e) {
|
||||
isLoading = false;
|
||||
trackError(e, Submit.OrganizationCreate);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: tierToPlan(BillingPlan.FREE)?.name,
|
||||
budget_cap_enabled: false,
|
||||
members_invited: 0
|
||||
});
|
||||
|
||||
if (org) {
|
||||
const teamId = org.$id;
|
||||
if (data.organization) {
|
||||
const teamId = data.organization.$id;
|
||||
try {
|
||||
const project = await sdk.forConsole.projects.create(
|
||||
id ?? ID.unique(),
|
||||
|
||||
@@ -1,9 +1,55 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { isOrganization, tierToPlan } from '$lib/stores/billing';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$service-worker';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
return {
|
||||
regions: isCloud ? await sdk.forConsole.billing.listRegions() : null
|
||||
};
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { organizations } = await parent();
|
||||
if (!organizations?.total) {
|
||||
try {
|
||||
if (isCloud) {
|
||||
const org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal projects',
|
||||
BillingPlan.FREE,
|
||||
null,
|
||||
null
|
||||
);
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: tierToPlan(BillingPlan.FREE)?.name,
|
||||
budget_cap_enabled: false,
|
||||
members_invited: 0
|
||||
});
|
||||
|
||||
if (isOrganization(org)) {
|
||||
return {
|
||||
organization: org,
|
||||
regions: await sdk.forConsole.billing.listRegions(org.$id)
|
||||
};
|
||||
} else {
|
||||
const e = new Error(org.message, {
|
||||
cause: org
|
||||
});
|
||||
trackError(e, Submit.OrganizationCreate);
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
organization: await sdk.forConsole.teams.create(
|
||||
ID.unique(),
|
||||
'Personal projects'
|
||||
),
|
||||
regions: null
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
trackError(e, Submit.OrganizationCreate);
|
||||
}
|
||||
} else {
|
||||
redirect(303, `${base}/console/organization-${organizations.teams[0].$id}`);
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -53,6 +53,6 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button on:click={moveDomain} disabled={!selectedOrg}>Move</Button>
|
||||
<Button on:click={moveDomain} disabled={!selectedOrg || options?.length === 0}>Move</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
+19
-6
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Empty, EmptySearch, PaginationWithLimit } from '$lib/components';
|
||||
import { Filters, hasPageQueries, queries } from '$lib/components/filters';
|
||||
import ViewSelector from '$lib/components/viewSelector.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import type { ColumnType } from '$lib/helpers/types';
|
||||
import type { Column, ColumnType } from '$lib/helpers/types';
|
||||
import { Container } from '$lib/layout';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { canWriteCollections, canWriteDocuments } from '$lib/stores/roles';
|
||||
@@ -15,14 +14,16 @@
|
||||
import CreateAttribute from './createAttribute.svelte';
|
||||
import { collection, columns, isCsvImportInProgress } from './store';
|
||||
import Table from './table.svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { writable } from 'svelte/store';
|
||||
import FilePicker from '$lib/components/filePicker.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { base } from '$app/paths';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -30,6 +31,8 @@
|
||||
let showCreateAttribute = false;
|
||||
let selectedAttribute: Option['name'] = null;
|
||||
|
||||
const filterColumns = writable<Column[]>([]);
|
||||
|
||||
$: selected = preferences.getCustomCollectionColumns(page.params.collection);
|
||||
$: columns.set(
|
||||
$collection.attributes.map((attribute) => ({
|
||||
@@ -42,6 +45,16 @@
|
||||
elements: 'elements' in attribute ? attribute.elements : null
|
||||
}))
|
||||
);
|
||||
$: filterColumns.set([
|
||||
...$columns,
|
||||
...['$id', '$createdAt', '$updatedAt'].map((id) => ({
|
||||
id,
|
||||
title: id,
|
||||
show: true,
|
||||
type: (id === '$id' ? 'string' : 'datetime') as ColumnType
|
||||
}))
|
||||
]);
|
||||
|
||||
$: hasAttributes = !!$collection.attributes.length;
|
||||
$: hasValidAttributes = $collection?.attributes?.some((attr) => attr.status === 'available');
|
||||
|
||||
|
||||
+104
-106
@@ -1,140 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, BoxAvatar } from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { doc } from './store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import Delete from './delete.svelte';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { Permissions } from '$lib/components/permissions';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { collection } from '../store';
|
||||
import { Alert, Link } from '@appwrite.io/pink-svelte';
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { doc } from './store';
|
||||
import { collection, type Attributes } from '../store';
|
||||
import AttributeItem from './attributeItem.svelte';
|
||||
import { isRelationship, isRelationshipToMany } from './attributes/store';
|
||||
import { deepClone } from '$lib/helpers/object';
|
||||
|
||||
let showDelete = false;
|
||||
let permissions = $doc?.$permissions;
|
||||
let arePermsDisabled = true;
|
||||
let showPermissionAlert = true;
|
||||
const databaseId = page.params.database;
|
||||
const collectionId = page.params.collection;
|
||||
const documentId = page.params.document;
|
||||
const editing = true;
|
||||
|
||||
async function updatePermissions() {
|
||||
function initWork() {
|
||||
const prohibitedKeys = [
|
||||
'$id',
|
||||
'$collection',
|
||||
'$collectionId',
|
||||
'$databaseId',
|
||||
'$createdAt',
|
||||
'$updatedAt'
|
||||
];
|
||||
|
||||
const filteredKeys = Object.keys($doc).filter((key) => {
|
||||
return !prohibitedKeys.includes(key);
|
||||
});
|
||||
|
||||
const result = filteredKeys.reduce((obj, key) => {
|
||||
obj[key] = $doc[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return writable(deepClone(result as Models.Document));
|
||||
}
|
||||
|
||||
const work = initWork();
|
||||
|
||||
async function updateData() {
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.databases.updateDocument(
|
||||
$doc.$databaseId,
|
||||
$doc.$collectionId,
|
||||
$doc.$id,
|
||||
$doc.data,
|
||||
permissions
|
||||
databaseId,
|
||||
collectionId,
|
||||
documentId,
|
||||
$work,
|
||||
$work.$permissions
|
||||
);
|
||||
await invalidate(Dependencies.DOCUMENT);
|
||||
arePermsDisabled = true;
|
||||
|
||||
invalidate(Dependencies.DOCUMENT);
|
||||
trackEvent(Submit.DocumentUpdate);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
message: 'Document has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.DocumentUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.DocumentUpdatePermissions);
|
||||
trackError(error, Submit.DocumentUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (permissions) {
|
||||
if (symmetricDifference(permissions, $doc.$permissions).length) {
|
||||
arePermsDisabled = false;
|
||||
} else arePermsDisabled = true;
|
||||
function compareAttributes(
|
||||
attribute: Attributes,
|
||||
$work: Models.Document,
|
||||
$doc: Models.Document
|
||||
) {
|
||||
if (!attribute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const workAttribute = $work?.[attribute.key];
|
||||
const docAttribute = $doc?.[attribute.key];
|
||||
|
||||
if (attribute.array) {
|
||||
return !symmetricDifference(Array.from(workAttribute), Array.from(docAttribute)).length;
|
||||
}
|
||||
|
||||
if (isRelationship(attribute)) {
|
||||
if (isRelationshipToMany(attribute as Models.AttributeRelationship)) {
|
||||
const workIds = workAttribute.map((doc: string | Record<string, unknown>) =>
|
||||
typeof doc === 'string' ? doc : doc.$id
|
||||
);
|
||||
|
||||
const relatedIds = docAttribute.map((doc: string | Record<string, unknown>) =>
|
||||
typeof doc === 'string' ? doc : doc.$id
|
||||
);
|
||||
return !symmetricDifference(workIds, relatedIds).length;
|
||||
} else {
|
||||
const workId =
|
||||
typeof workAttribute === 'string' ? workAttribute : workAttribute?.$id;
|
||||
const relatedId =
|
||||
typeof docAttribute === 'string' ? docAttribute : docAttribute?.$id;
|
||||
|
||||
return workId === relatedId;
|
||||
}
|
||||
}
|
||||
return workAttribute === docAttribute;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Document - Appwrite</title>
|
||||
<title>Data - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Container>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Metadata</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<div>
|
||||
<p>Created: {toLocaleDateTime($doc.$createdAt)}</p>
|
||||
<p>Last updated: {toLocaleDateTime($doc.$updatedAt)}</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Permissions</svelte:fragment>
|
||||
A user requires appropriate permissions at either the <b>collection level</b> or
|
||||
<b>document level</b> to access a document. If no permissions are configured, no user can
|
||||
access the document
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/docs/products/databases/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Learn more</Link.Anchor>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $collection.documentSecurity}
|
||||
{#if showPermissionAlert}
|
||||
<Alert.Inline
|
||||
status="info"
|
||||
dismissible
|
||||
on:dismiss={() => (showPermissionAlert = false)}>
|
||||
<svelte:fragment slot="title">Document security is enabled</svelte:fragment>
|
||||
<p class="text">
|
||||
Users will be able to access this document if they have been granted <b
|
||||
>either document or collection permissions.
|
||||
</b>
|
||||
</p>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
{#if permissions}
|
||||
<Permissions bind:permissions />
|
||||
{/if}
|
||||
{:else}
|
||||
<Alert.Inline status="info">
|
||||
<svelte:fragment slot="title">Document security is disabled</svelte:fragment>
|
||||
<p class="text">
|
||||
If you want to assign document permissions. Go to <Link.Anchor
|
||||
href={`./settings`}>Collection settings</Link.Anchor> and enable document
|
||||
security. Otherwise, only collection permissions will be used.
|
||||
</p>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
{#if $collection?.attributes?.length}
|
||||
{#each $collection.attributes as attribute}
|
||||
{@const label = attribute.key}
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">{label}</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
disabled={arePermsDisabled}
|
||||
on:click={() => {
|
||||
updatePermissions();
|
||||
}}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Delete document</svelte:fragment>
|
||||
The document will be permanently deleted, including all the data within it. This action is irreversible.
|
||||
<svelte:fragment slot="aside">
|
||||
<BoxAvatar>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">{$doc.$id}</h6>
|
||||
<svelte:fragment slot="aside">
|
||||
<AttributeItem {attribute} bind:formValues={$work} {label} {editing} />
|
||||
</svelte:fragment>
|
||||
<p>
|
||||
Last updated: {toLocaleDateTime($doc.$updatedAt)}
|
||||
</p>
|
||||
</BoxAvatar>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
disabled={compareAttributes(attribute, $work, $doc)}
|
||||
on:click={() => updateData()}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{/each}
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<Delete bind:showDelete />
|
||||
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { CardGrid } from '$lib/components';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { doc } from '../store';
|
||||
import { collection, type Attributes } from '../../store';
|
||||
import { Container } from '$lib/layout';
|
||||
import AttributeItem from '../attributeItem.svelte';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { isRelationship, isRelationshipToMany } from '../attributes/store';
|
||||
import { deepClone } from '$lib/helpers/object';
|
||||
|
||||
const databaseId = page.params.database;
|
||||
const collectionId = page.params.collection;
|
||||
const documentId = page.params.document;
|
||||
const editing = true;
|
||||
|
||||
function initWork() {
|
||||
const prohibitedKeys = [
|
||||
'$id',
|
||||
'$collection',
|
||||
'$collectionId',
|
||||
'$databaseId',
|
||||
'$createdAt',
|
||||
'$updatedAt'
|
||||
];
|
||||
|
||||
const filteredKeys = Object.keys($doc).filter((key) => {
|
||||
return !prohibitedKeys.includes(key);
|
||||
});
|
||||
|
||||
const result = filteredKeys.reduce((obj, key) => {
|
||||
obj[key] = $doc[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return writable(deepClone(result as Models.Document));
|
||||
}
|
||||
|
||||
const work = initWork();
|
||||
|
||||
async function updateData() {
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.databases.updateDocument(
|
||||
databaseId,
|
||||
collectionId,
|
||||
documentId,
|
||||
$work,
|
||||
$work.$permissions
|
||||
);
|
||||
|
||||
invalidate(Dependencies.DOCUMENT);
|
||||
trackEvent(Submit.DocumentUpdate);
|
||||
addNotification({
|
||||
message: 'Document has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.DocumentUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
function compareAttributes(
|
||||
attribute: Attributes,
|
||||
$work: Models.Document,
|
||||
$doc: Models.Document
|
||||
) {
|
||||
if (!attribute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const workAttribute = $work?.[attribute.key];
|
||||
const docAttribute = $doc?.[attribute.key];
|
||||
|
||||
if (attribute.array) {
|
||||
return !symmetricDifference(Array.from(workAttribute), Array.from(docAttribute)).length;
|
||||
}
|
||||
|
||||
if (isRelationship(attribute)) {
|
||||
if (isRelationshipToMany(attribute as Models.AttributeRelationship)) {
|
||||
const workIds = workAttribute.map((doc: string | Record<string, unknown>) =>
|
||||
typeof doc === 'string' ? doc : doc.$id
|
||||
);
|
||||
|
||||
const relatedIds = docAttribute.map((doc: string | Record<string, unknown>) =>
|
||||
typeof doc === 'string' ? doc : doc.$id
|
||||
);
|
||||
return !symmetricDifference(workIds, relatedIds).length;
|
||||
} else {
|
||||
const workId =
|
||||
typeof workAttribute === 'string' ? workAttribute : workAttribute?.$id;
|
||||
const relatedId =
|
||||
typeof docAttribute === 'string' ? docAttribute : docAttribute?.$id;
|
||||
|
||||
return workId === relatedId;
|
||||
}
|
||||
}
|
||||
return workAttribute === docAttribute;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Data - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Container>
|
||||
{#if $collection?.attributes?.length}
|
||||
{#each $collection.attributes as attribute}
|
||||
{@const label = attribute.key}
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">{label}</svelte:fragment>
|
||||
<svelte:fragment slot="aside">
|
||||
<AttributeItem {attribute} bind:formValues={$work} {label} {editing} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
disabled={compareAttributes(attribute, $work, $doc)}
|
||||
on:click={() => updateData()}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
{/each}
|
||||
{/if}
|
||||
</Container>
|
||||
+7
-7
@@ -14,20 +14,20 @@
|
||||
const tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Overview',
|
||||
event: 'overview'
|
||||
},
|
||||
{
|
||||
href: `${path}/data`,
|
||||
title: 'Data',
|
||||
event: 'data',
|
||||
hasChildren: true
|
||||
event: 'data'
|
||||
},
|
||||
{
|
||||
href: `${path}/activity`,
|
||||
title: 'Activity',
|
||||
event: 'activity',
|
||||
hasChildren: true
|
||||
},
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
title: 'Settings',
|
||||
event: 'settings',
|
||||
hasChildren: true
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, BoxAvatar, Alert } from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { doc } from '../store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import Delete from '../delete.svelte';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { Permissions } from '$lib/components/permissions';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { collection } from '../../store';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
let showDelete = false;
|
||||
let permissions = $doc?.$permissions;
|
||||
let arePermsDisabled = true;
|
||||
let showPermissionAlert = true;
|
||||
|
||||
async function updatePermissions() {
|
||||
try {
|
||||
await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.databases.updateDocument(
|
||||
$doc.$databaseId,
|
||||
$doc.$collectionId,
|
||||
$doc.$id,
|
||||
$doc.data,
|
||||
permissions
|
||||
);
|
||||
await invalidate(Dependencies.DOCUMENT);
|
||||
arePermsDisabled = true;
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.DocumentUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.DocumentUpdatePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
$: if (permissions) {
|
||||
arePermsDisabled = !symmetricDifference(permissions, $doc.$permissions).length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Document - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<Container>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Metadata</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<div>
|
||||
<p>Created: {toLocaleDateTime($doc.$createdAt)}</p>
|
||||
<p>Last updated: {toLocaleDateTime($doc.$updatedAt)}</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Permissions</svelte:fragment>
|
||||
|
||||
<p>
|
||||
A user requires appropriate permissions at either the <b>collection level</b> or
|
||||
<b>document level</b> to access a document. If no permissions are configured, no user
|
||||
can access the document
|
||||
<a
|
||||
href="https://appwrite.io/docs/products/databases/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more about database permissions</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $collection.documentSecurity}
|
||||
{#if showPermissionAlert}
|
||||
<Alert type="info" dismissible on:dismiss={() => (showPermissionAlert = false)}>
|
||||
<svelte:fragment slot="title">Document security is enabled</svelte:fragment>
|
||||
<p class="text">
|
||||
Users will be able to access this document if they have been granted <b
|
||||
>either document or collection permissions.
|
||||
</b>
|
||||
</p>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if permissions}
|
||||
<Permissions bind:permissions />
|
||||
{/if}
|
||||
{:else}
|
||||
<Alert type="info">
|
||||
<svelte:fragment slot="title">Document security is disabled</svelte:fragment>
|
||||
<p class="text">
|
||||
If you want to assign document permissions. Go to Collection settings and
|
||||
enable document security. Otherwise, only collection permissions will be
|
||||
used.
|
||||
</p>
|
||||
</Alert>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
disabled={arePermsDisabled}
|
||||
on:click={() => {
|
||||
updatePermissions();
|
||||
}}>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<CardGrid>
|
||||
<svelte:fragment slot="title">Delete document</svelte:fragment>
|
||||
<p>
|
||||
The document will be permanently deleted, including all the data within it. This action
|
||||
is irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<BoxAvatar>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">{$doc.$id}</h6>
|
||||
</svelte:fragment>
|
||||
<p>
|
||||
Last updated: {toLocaleDateTime($doc.$updatedAt)}
|
||||
</p>
|
||||
</BoxAvatar>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Container>
|
||||
|
||||
<Delete bind:showDelete />
|
||||
+1
@@ -111,6 +111,7 @@
|
||||
<Upload.List
|
||||
files={Array.from(files).map((f) => {
|
||||
return {
|
||||
...f,
|
||||
size: f.size,
|
||||
name: f.name,
|
||||
removable: true,
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
id ?? ID.unique(),
|
||||
projectName,
|
||||
selectedOrg,
|
||||
isCloud ? (region as Region) : Region.Default
|
||||
isCloud ? (region as Region) : Region.Fra
|
||||
);
|
||||
trackEvent(Submit.ProjectCreate, {
|
||||
customId: !!id,
|
||||
|
||||
Reference in New Issue
Block a user