Merge branch 'main' of github.com:appwrite/console into feat-credit-flow

This commit is contained in:
Arman committed 2024-06-05 12:42:24 +02:00
commit 2a6a5e97ee
20 files changed
+970 -719

No files matched your search

+660 -651
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -20,7 +20,7 @@
<ul
class="grid-box common-section u-margin-block-start-32"
style={`--grid-gap:1.5rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
style={`--grid-gap:1.5rem; --grid-item-size-small-screens: 18rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
data-private>
<slot />
+2
View File
@@ -13,6 +13,7 @@
export let fullWidth = false;
export let wrapperFullWidth = false;
export let position: 'relative' | 'static' = 'relative';
export let noMaxWidthList = false;
</script>
<Drop
@@ -35,6 +36,7 @@
}`}>
{#if $$slots.list}
<section
class:u-max-width-none={noMaxWidthList}
class:u-overflow-y-auto={scrollable}
class:u-max-height-200={scrollable}
class="drop-section">
+140 -45
View File
@@ -1,22 +1,30 @@
<script lang="ts">
import { Button, InputNumber } from '$lib/elements/forms';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import InputText from '$lib/elements/forms/inputText.svelte';
import {
Button,
InputNumber,
InputSelect,
InputText,
InputTags,
FormList,
InputSelectCheckbox
} from '$lib/elements/forms';
import { Query } from '@appwrite.io/console';
import { createEventDispatcher } from 'svelte';
import { tags, type Operator, queries } from './store';
import { createEventDispatcher, onMount } from 'svelte';
import { tags, type Operator, queries, type TagValue } from './store';
import type { Column } from '$lib/helpers/types';
import type { Writable } from 'svelte/store';
import { tooltip } from '$lib/actions/tooltip';
export let columns: Writable<Column[]>;
export let columnId: string | null = null;
const dispatch = createEventDispatcher<{
clear: void;
apply: { applied: number };
}>();
let columnId: string | null = null;
$: column = $columns.find((c) => c.id === columnId) as Column;
let arrayValues: string[] = [];
dispatch('apply', { applied: $tags.length });
@@ -36,7 +44,7 @@
toTag: (attribute, input) => `**${attribute}** greater than **${input}**`,
types: ['integer', 'double', 'datetime']
},
'greater than or equal to': {
'greater than or equal': {
toQuery: (attr, input) => Query.greaterThanEqual(attr, Number(input)),
toTag: (attribute, input) => `**${attribute}** greater than or equal to **${input}**`,
types: ['integer', 'double', 'datetime']
@@ -46,7 +54,7 @@
toTag: (attribute, input) => `**${attribute}** less than **${input}**`,
types: ['integer', 'double', 'datetime']
},
'less than or equal to': {
'less than or equal': {
toQuery: Query.lessThanEqual,
toTag: (attribute, input) => `**${attribute}** less than or equal to **${input}**`,
types: ['integer', 'double', 'datetime']
@@ -56,22 +64,36 @@
toTag: (attribute, input) => `**${attribute}** equal to **${input}**`,
types: ['string', 'integer', 'double', 'boolean']
},
'not equal to': {
'not equal': {
toQuery: Query.notEqual,
toTag: (attribute, input) => `**${attribute}** not equal to **${input}**`,
types: ['string', 'integer', 'double', 'boolean']
},
'not null': {
'is not null': {
toQuery: Query.isNotNull,
toTag: (attribute) => `**${attribute}** is not null`,
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'],
hideInput: true
},
null: {
'is null': {
toQuery: Query.isNull,
toTag: (attribute) => `**${attribute}** is null`,
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'],
hideInput: true
},
contains: {
toQuery: Query.contains,
toTag: (attribute, input) => {
if (Array.isArray(input) && input.length > 2) {
return {
value: input,
tag: `**${attribute}** contains **${formatArray(input)}** `
};
} else {
return `**${attribute}** contains **${input}**`;
}
},
types: ['string', 'integer', 'double', 'boolean', 'datetime', 'enum']
}
};
@@ -93,23 +115,48 @@
/* eslint @typescript-eslint/no-explicit-any: 'off' */
let value: any = null;
$: {
columnId;
value = null;
}
onMount(() => {
value = column?.array ? [] : null;
});
// This Map is keyed by tags, and has a query as the value
function addFilter() {
if (!column || !operator) return;
queries.addFilter({ column, operator, value: value ?? '' });
value = null;
if (column.array) {
queries.addFilter({ column, operator, value: arrayValues });
columnId = null;
arrayValues = [];
} else {
queries.addFilter({ column, operator, value: value ?? '' });
columnId = null;
value = null;
}
}
function tagFormat(node: HTMLElement) {
node.innerHTML = node.innerHTML.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
}
function formatArray(array: string[]) {
if (!array?.length) return;
if (array.length > 2) {
return `${array[0]} or ${array.length - 1} others`;
} else {
return array.join(' or ');
}
}
function isTypeTagValue(obj: any): obj is TagValue {
if (typeof obj === 'string') return false;
return (
obj &&
typeof obj.tag === 'string' &&
(typeof obj.value === 'string' ||
typeof obj.value === 'number' ||
Array.isArray(obj.value))
);
}
$: isDisabled = !operator;
</script>
@@ -134,23 +181,55 @@
bind:value={operatorKey} />
</ul>
{#if column && operator && !operator?.hideInput}
<ul class="u-margin-block-start-8">
{#if column.type === 'integer' || column.type === 'double'}
<InputNumber id="value" bind:value placeholder="Enter value" />
{:else if column.type === 'boolean'}
<InputSelect
id="value"
placeholder="Select a value"
required={true}
options={[
{ label: 'True', value: true },
{ label: 'False', value: false }
].filter(Boolean)}
bind:value />
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
{/if}
</ul>
{#if column?.array}
<FormList class="u-margin-block-start-8">
{#if column.format === 'enum'}
<InputSelectCheckbox
name="value"
bind:tags={arrayValues}
placeholder="Select value"
options={column?.elements?.map((value) => ({
label: value,
value,
checked: arrayValues.includes(value)
}))}>
</InputSelectCheckbox>
{:else}
<InputTags
label="values"
showLabel={false}
id="value"
bind:tags={arrayValues}
placeholder="Enter values" />
{/if}
</FormList>
{:else}
<ul class="u-margin-block-start-8">
{#if column.format === 'enum'}
<InputSelect
id="value"
bind:value
placeholder="Select value"
options={column?.elements?.map((value) => ({ label: value, value }))}
label="Value"
showLabel={false} />
{:else if column.type === 'integer' || column.type === 'double'}
<InputNumber id="value" bind:value placeholder="Enter value" />
{:else if column.type === 'boolean'}
<InputSelect
id="value"
placeholder="Select a value"
required={true}
options={[
{ label: 'True', value: true },
{ label: 'False', value: false }
].filter(Boolean)}
bind:value />
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
{/if}
</ul>
{/if}
{/if}
<Button text disabled={isDisabled} class="u-margin-block-start-4" submit>
<i class="icon-plus" />
@@ -160,16 +239,32 @@
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
{#each $tags as tag (tag)}
<button
class="tag"
on:click={() => {
queries.removeFilter(tag);
}}>
<span class="text" use:tagFormat>
{tag}
</span>
<i class="icon-x" />
</button>
{#if isTypeTagValue(tag)}
<button
use:tooltip={{
content: tag?.value?.toString()
}}
class="tag"
on:click={() => {
queries.removeFilter(tag);
}}>
<span class="text" use:tagFormat>
{tag.tag}
</span>
<i class="icon-x" />
</button>
{:else}
<button
class="tag"
on:click={() => {
queries.removeFilter(tag);
}}>
<span class="text" use:tagFormat>
{tag}
</span>
<i class="icon-x" />
</button>
{/if}
{/each}
</ul>
</div>
+15 -3
View File
@@ -9,11 +9,13 @@
export let query = '[]';
export let columns: Writable<Column[]>;
export let disabled = false;
export let fullWidthMobile = false;
const parsedQueries = queryParamToMap(query);
queries.set(parsedQueries);
let selectedColumn: string | null = null;
// We need to separate them so we don't trigger Drop's handlers
let showFiltersDesktop = false;
let showFiltersMobile = false;
@@ -25,11 +27,20 @@
showFiltersDesktop = false;
showFiltersMobile = false;
});
function clearAll() {
selectedColumn = null;
queries.clearAll();
}
$: if (!showFiltersDesktop && !showFiltersMobile) {
selectedColumn = null;
}
</script>
<div class="is-not-mobile">
<Drop bind:show={showFiltersDesktop} noArrow>
<Button secondary on:click={() => (showFiltersDesktop = !showFiltersDesktop)}>
<Button secondary on:click={() => (showFiltersDesktop = !showFiltersDesktop)} {disabled}>
<i class="icon-filter u-opacity-50" />
Filters
{#if applied > 0}
@@ -42,12 +53,13 @@
<div class="dropped card">
<p>Apply filter rules to refine the table view</p>
<Content
bind:columnId={selectedColumn}
{columns}
on:apply={(e) => (applied = e.detail.applied)}
on:clear={() => (applied = 0)} />
<hr />
<div class="u-flex u-margin-block-start-16 u-main-end u-gap-8">
<Button text on:click={queries.clearAll}>Clear all</Button>
<Button text on:click={clearAll}>Clear all</Button>
<Button on:click={queries.apply} disabled={!$queriesAreDirty}>Apply</Button>
</div>
</div>
@@ -76,7 +88,7 @@
on:apply={(e) => (applied = e.detail.applied)}
on:clear={() => (applied = 0)} />
<svelte:fragment slot="footer">
<Button text on:click={queries.clearAll}>Clear all</Button>
<Button text on:click={clearAll}>Clear all</Button>
<Button on:click={queries.apply} disabled={!$queriesAreDirty}>Apply</Button
></svelte:fragment>
</Modal>
+11 -6
View File
@@ -5,14 +5,19 @@ import { page } from '$app/stores';
import deepEqual from 'deep-equal';
import type { Column, ColumnType } from '$lib/helpers/types';
export type TagValue = {
tag: string;
value: string | number | string[];
};
export type Operator = {
toTag: (attribute: string, input?: string | number) => string;
toQuery: (attribute: string, input?: string | number) => string;
toTag: (attribute: string, input?: string | number | string[]) => string | TagValue;
toQuery: (attribute: string, input?: string | number | string[]) => string;
types: ColumnType[];
hideInput?: boolean;
};
export function mapToQueryParams(map: Map<string, string>) {
export function mapToQueryParams(map: Map<string | TagValue, string>) {
return encodeURIComponent(JSON.stringify(Array.from(map.entries())));
}
@@ -22,13 +27,13 @@ export function queryParamToMap(queryParam: string) {
return new Map(queries);
}
function initQueries(initialValue = new Map<string, string>()) {
function initQueries(initialValue = new Map<string | TagValue, string>()) {
const queries = writable(initialValue);
type AddFilterArgs = {
operator: Operator;
column: Column;
value: string | number;
value: string | number | string[];
};
function addFilter({ column, operator, value }: AddFilterArgs) {
@@ -38,7 +43,7 @@ function initQueries(initialValue = new Map<string, string>()) {
});
}
function removeFilter(tag: string) {
function removeFilter(tag: string | TagValue) {
queries.update((map) => {
map.delete(tag);
return map;
+1
View File
@@ -31,4 +31,5 @@ export { default as InputDate } from './inputDate.svelte';
export { default as InputDateRange } from './inputDateRange.svelte';
export { default as InputTime } from './inputTime.svelte';
export { default as InputDigits } from './inputDigits.svelte';
export { default as InputSelectCheckbox } from './inputSelectCheckbox.svelte';
export { default as InputFilePicker } from './inputFilePicker.svelte';
@@ -0,0 +1,88 @@
<script lang="ts">
import { DropList } from '$lib/components';
import { SelectSearchCheckbox } from '..';
type Option = {
value: string;
label: string;
checked: boolean;
};
export let name: string;
export let tags: string[] = [];
export let placeholder: string;
export let options: Option[] = [];
let search = '';
let input: HTMLInputElement;
let show = false;
function addValue(option: Option) {
let tag = option.value.trim();
if (tag.length === 0 || tags.includes(tag)) return;
tags = [...tags, tag];
option.checked = true;
clearSearch();
}
function removeValue(option: Option) {
tags = tags.filter((tag) => tag !== option.value);
option.checked = false;
}
function clearSearch() {
search = '';
input.value = '';
}
$: filteredOptions = search
? options.filter((option) => option.label.toLowerCase().includes(search.toLowerCase()))
: options;
//TODO: Debounce search and open the droplist when the user starts typing
</script>
<DropList bind:show noStyle noArrow scrollable placement="bottom-end" fixed noMaxWidthList>
<button
class="tags-input u-position-relative u-cursor-pointer"
type="button"
on:click={() => (show = !show)}>
<div class="tags">
<ul class="tags-list">
{#each tags as tag}
<li class="tags-item">
<div class="input-tag">
<span class="tag-text">{tag}</span>
</div>
</li>
{/each}
</ul>
</div>
<input
class="tags-input-text u-cursor-text"
{placeholder}
bind:value={search}
bind:this={input} />
<span
class:icon-cheveron-up={show}
class:icon-cheveron-down={!show}
class="u-position-absolute u-inset-block-start-4 u-inset-inline-end-12"
aria-hidden="true"></span>
</button>
<svelte:fragment slot="list">
{#each filteredOptions as option (option.value + option.checked)}
<SelectSearchCheckbox
on:click={() => (option?.checked ? removeValue(option) : addValue(option))}
bind:value={option.checked}>
{option.label}
</SelectSearchCheckbox>
{:else}
<li class="drop-list-item">
<span class="text">There are no {name} that match your search</span>
</li>
{/each}
</svelte:fragment>
</DropList>
+1
View File
@@ -1,3 +1,4 @@
export { default as Pill } from './pill.svelte';
export { default as SelectSearchItem } from './selectSearchItem.svelte';
export { default as Flag } from './flag.svelte';
export { default as SelectSearchCheckbox } from './selectSearchCheckbox.svelte';
@@ -0,0 +1,14 @@
<script lang="ts">
// export let data: string[] = [];
export let value = false;
</script>
<li class="drop-list-item">
<button
class="drop-button u-flex u-cross-center u-gap-12"
on:click|preventDefault
type="button">
<input type="checkbox" class="is-small" bind:checked={value} />
<slot />
</button>
</li>
+12 -1
View File
@@ -13,7 +13,15 @@ export function isHTMLInputElement(el: unknown): el is HTMLInputElement {
// eslint-disable-next-line @typescript-eslint/ban-types
export type Prettify<T> = T & {};
const columnTypes = ['string', 'integer', 'double', 'boolean', 'datetime', 'relationship'] as const;
const columnTypes = [
'string',
'integer',
'double',
'boolean',
'datetime',
'relationship',
'enum'
] as const;
export type ColumnType = (typeof columnTypes)[number];
export type Column = {
id: string;
@@ -28,6 +36,9 @@ export type Column = {
* Set to false to disable filtering for this column
*/
filter?: boolean;
array?: boolean;
format?: string;
elements?: string[];
};
export function isValueOfStringEnum<T extends Record<string, string>>(
+1 -1
View File
@@ -9,7 +9,7 @@
<footer class="main-footer u-cross-center">
<div class="main-footer-start">
<ul class="inline-links is-no-padding-first-and-last u-x-small">
<ul class="inline-links is-no-padding-first-and-last u-x-small u-flex-wrap u-row-gap-16">
<li class="inline-links-item" style="line-height: 1.02;">
<div class="u-flex u-cross-center u-gap-8">
{#if isCloud}
@@ -29,6 +29,7 @@
} catch (e) {
error = true;
trackError(e.message, Submit.DomainCreate);
throw new Error(e.message);
}
}
</script>
+2 -1
View File
@@ -34,7 +34,6 @@
import { project } from './project-[project]/store';
import { feedback } from '$lib/stores/feedback';
import { VARS, hasStripePublicKey, isCloud } from '$lib/system';
import { loadStripe } from '@stripe/stripe-js';
import { stripe } from '$lib/stores/stripe';
import MobileSupportModal from './wizard/support/mobileSupportModal.svelte';
import { showSupportModal } from './wizard/support/store';
@@ -236,6 +235,8 @@
}, INTERVAL);
if (isCloud && hasStripePublicKey) {
const loadStripe = (await import('@stripe/stripe-js')).loadStripe;
$stripe = await loadStripe(VARS.STRIPE_PUBLIC_KEY);
await checkForMissingPaymentMethod();
}
@@ -121,7 +121,7 @@
<PlanSummary />
<PaymentHistory />
<PaymentMethods />
<BillingAddress billingAddress={data.billingAddress} />
<BillingAddress billingAddress={data?.billingAddress} />
<TaxId />
<BudgetCap />
{#if $organization?.billingPlan !== BillingPlan.STARTER && !!$organization?.billingBudget}
@@ -15,10 +15,14 @@ export const load: PageLoad = async ({ parent, depends }) => {
let billingAddress: Address = null;
const billingAddressId = (organization as Organization)?.billingAddressId;
if (billingAddressId) {
billingAddress = await sdk.forConsole.billing.getOrganizationBillingAddress(
organization.$id,
billingAddressId
);
try {
billingAddress = await sdk.forConsole.billing.getOrganizationBillingAddress(
organization.$id,
billingAddressId
);
} catch {
billingAddress = null;
}
}
return {
@@ -28,7 +28,10 @@
id: attribute.key,
title: attribute.key,
type: attribute.type as ColumnType,
show: selected?.includes(attribute.key) ?? true
show: selected?.includes(attribute.key) ?? true,
array: attribute?.array,
format: 'format' in attribute && attribute?.format === 'enum' ? attribute.format : null,
elements: 'elements' in attribute ? attribute.elements : null
}))
);
@@ -54,7 +57,7 @@
</Button>
</div>
<Filters query={data.query} {columns} />
<Filters query={data.query} {columns} disabled={!(hasAttributes && hasValidAttributes)} />
<div class="u-flex u-main-end u-gap-16">
<ViewSelector
@@ -91,7 +91,11 @@
id: attribute.key,
title: attribute.key,
type: attribute.type as ColumnType,
show: selected?.includes(attribute.key) ?? true
show: selected?.includes(attribute.key) ?? true,
array: attribute?.array,
format:
'format' in attribute && attribute?.format === 'enum' ? attribute.format : null,
elements: 'elements' in attribute ? attribute.elements : null
}))
);
}
@@ -34,7 +34,7 @@
`functions.${$page.params.function}.deployments.${$page.params.deployment}.update`
)
) {
logs = message.payload.buildLogs;
logs = message.payload['logs'];
if (message.payload.status === 'ready') {
invalidate(Dependencies.DEPLOYMENT);
}
@@ -413,7 +413,7 @@
label="Allowed extensions"
placeholder="Allowed file extensions (mp4, jpg, pdf, etc.)"
bind:tags={extensions} />
<li class="u-flex u-gap-12 u-margin-block-start-8">
<li class="u-flex u-gap-12 u-margin-block-start-8 with-scroll">
{#each suggestedExtensions as ext}
<Pill
selected={extensions.includes(ext)}