mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into feat-improve-domain-flows
This commit is contained in:
@@ -41,7 +41,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 }}"
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
"PUBLIC_CONSOLE_MODE=cloud"
|
||||
"PUBLIC_CONSOLE_FEATURE_FLAGS="
|
||||
"PUBLIC_APPWRITE_MULTI_REGION=true"
|
||||
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
|
||||
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
|
||||
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
|
||||
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
|
||||
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ RUN corepack prepare pnpm@10.0.0 --activate
|
||||
ADD ./package.json /app/package.json
|
||||
ADD ./pnpm-lock.yaml /app/pnpm-lock.yaml
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
ADD ./build.js /app/build.js
|
||||
ADD ./tsconfig.json /app/tsconfig.json
|
||||
|
||||
@@ -106,11 +106,11 @@
|
||||
{#if hasSearch}
|
||||
<SearchQuery placeholder={searchPlaceholder} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
{#if hasFilters && $columns?.length}
|
||||
<QuickFilters {columns} {analyticsSource} {filterCols} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
{#if hasDisplaySettings}
|
||||
<ViewSelector ui="new" {view} {columns} {hideView} {hideColumns} />
|
||||
{/if}
|
||||
|
||||
@@ -21,14 +21,21 @@
|
||||
</script>
|
||||
|
||||
<Layout.Stack>
|
||||
<header class="form-header" class:hide-divider={!$$slots.subtitle}>
|
||||
<Typography.Title><slot name="title" /></Typography.Title>
|
||||
{#if $$slots.subtitle}
|
||||
<p>
|
||||
<slot name="subtitle" />
|
||||
</p>
|
||||
{/if}
|
||||
</header>
|
||||
{#if $$slots.title || $$slots.subtitle}
|
||||
<header
|
||||
class="form-header"
|
||||
class:hide-divider={!$$slots.subtitle}
|
||||
class:only-subtitle={!$$slots.title && $$slots.subtitle}>
|
||||
{#if $$slots.title}
|
||||
<Typography.Title><slot name="title" /></Typography.Title>
|
||||
{/if}
|
||||
{#if $$slots.subtitle}
|
||||
<p>
|
||||
<slot name="subtitle" />
|
||||
</p>
|
||||
{/if}
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
</Layout.Stack>
|
||||
@@ -42,4 +49,8 @@
|
||||
padding-block-end: 0;
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.only-subtitle {
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -83,12 +83,13 @@
|
||||
}
|
||||
|
||||
$: projectCreationDisabled =
|
||||
(isCloud && getServiceLimit('projects') <= data.projects.total) ||
|
||||
(isCloud && getServiceLimit('projects', null, data.currentPlan) <= data.projects.total) ||
|
||||
(isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE) ||
|
||||
!$canWriteProjects;
|
||||
|
||||
$: reachedProjectLimit = isCloud && getServiceLimit('projects') <= data.projects.total;
|
||||
$: projectsLimit = getServiceLimit('projects');
|
||||
$: reachedProjectLimit =
|
||||
isCloud && getServiceLimit('projects', null, data.currentPlan) <= data.projects.total;
|
||||
$: projectsLimit = getServiceLimit('projects', null, data.currentPlan);
|
||||
|
||||
$: $registerCommands([
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
let areMembersLimited: boolean = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
const limit = getServiceLimit('members') || Infinity;
|
||||
const limit = getServiceLimit('members', null, page.data.currentPlan) || Infinity;
|
||||
const isLimited = limit !== 0 && limit < Infinity;
|
||||
areMembersLimited =
|
||||
isCloud &&
|
||||
|
||||
@@ -243,11 +243,7 @@
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="messages" />
|
||||
{:else if data.search}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no messages that match your search.</p>
|
||||
</div>
|
||||
<EmptySearch target="messages" search={data.search}>
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button external href="https://appwrite.io/docs/products/messaging/messages" text>
|
||||
Documentation
|
||||
|
||||
+17
-19
@@ -21,15 +21,18 @@
|
||||
|
||||
const options = [
|
||||
{ label: 'Now', value: 'now' },
|
||||
{ label: 'Schedule', value: 'later' }
|
||||
{ label: 'Custom', value: 'later' }
|
||||
];
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
const dateOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
year: 'numeric'
|
||||
};
|
||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hourCycle: 'h23',
|
||||
timeZoneName: 'longGeneric'
|
||||
};
|
||||
@@ -57,28 +60,23 @@
|
||||
</script>
|
||||
|
||||
<Layout.Stack>
|
||||
<InputSelect id="when" required {options} bind:value={when} />
|
||||
<InputDate
|
||||
id="date"
|
||||
label="Date"
|
||||
disabled={when === 'now'}
|
||||
required={when === 'later'}
|
||||
min={minDate}
|
||||
bind:value={date} />
|
||||
<InputTime
|
||||
id="time"
|
||||
label="Time"
|
||||
disabled={when === 'now'}
|
||||
required={when === 'later'}
|
||||
min={minTime}
|
||||
bind:value={time} />
|
||||
<InputSelect id="when" label="Schedule" required {options} bind:value={when} />
|
||||
{#if when === 'later'}
|
||||
<Layout.Stack direction="row" gap="m">
|
||||
<InputDate id="date" min={minDate} bind:value={date} required />
|
||||
<InputTime id="time" min={minTime} bind:value={time} required />
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
<Helper type="neutral">
|
||||
{#if when === 'now'}
|
||||
The message will be sent immediately
|
||||
{:else if !dateTime || isNaN(dateTime.getTime())}
|
||||
The message will be sent later
|
||||
{:else}
|
||||
The message will be sent at {dateTime.toLocaleString('en', formatOptions)}
|
||||
The message will be sent on {dateTime.toLocaleDateString('en', dateOptions)} at {dateTime.toLocaleTimeString(
|
||||
'en',
|
||||
timeOptions
|
||||
)}
|
||||
{/if}
|
||||
</Helper>
|
||||
</Layout.Stack>
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@
|
||||
<Fieldset legend="Targets">
|
||||
<Targets type={MessagingProviderType.Email} bind:topics bind:targets />
|
||||
</Fieldset>
|
||||
<Fieldset legend="Schedule">
|
||||
<Fieldset legend="Settings">
|
||||
<Schedule bind:scheduledAt {targets} />
|
||||
</Fieldset>
|
||||
</Layout.Stack>
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@
|
||||
<Fieldset legend="Targets">
|
||||
<Targets type={MessagingProviderType.Push} bind:topics bind:targets />
|
||||
</Fieldset>
|
||||
<Fieldset legend="Schedule">
|
||||
<Fieldset legend="Settings">
|
||||
<Schedule bind:scheduledAt {targets} />
|
||||
</Fieldset>
|
||||
</Layout.Stack>
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@
|
||||
<Fieldset legend="Targets">
|
||||
<Targets type={MessagingProviderType.Sms} bind:topics bind:targets />
|
||||
</Fieldset>
|
||||
<Fieldset legend="Schedule">
|
||||
<Fieldset legend="Settings">
|
||||
<Schedule bind:scheduledAt {targets} />
|
||||
</Fieldset>
|
||||
</Layout.Stack>
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
import { page } from '$app/state';
|
||||
</script>
|
||||
|
||||
<Popover let:toggle padding="none" placement="bottom-end">
|
||||
<Popover let:toggle padding="none" placement="bottom-start">
|
||||
<slot {toggle}>
|
||||
<Button on:click={toggle} event="create_message">
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
|
||||
+7
-10
@@ -16,6 +16,7 @@
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Icon, Layout, Table, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { page } from '$app/state';
|
||||
import { Link } from '$lib/elements';
|
||||
|
||||
export let message: Models.Message;
|
||||
export let selectedTopicsById: Record<string, Models.Topic>;
|
||||
@@ -141,17 +142,13 @@
|
||||
<Empty on:click={() => (showTopics = true)}>Add a topic</Empty>
|
||||
{:else}
|
||||
<Card.Base padding="none">
|
||||
<PinkEmpty
|
||||
type="secondary"
|
||||
title="No topics were selected"
|
||||
description="No topics were selected for this message. Need a hand? Check out our documentation.">
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
secondary
|
||||
<PinkEmpty type="secondary" title="No topics were selected">
|
||||
<svelte:fragment slot="description">
|
||||
Need a hand? Check out our <Link
|
||||
variant="muted"
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
external>
|
||||
Documentation
|
||||
</Button>
|
||||
external>documentation</Link
|
||||
>.
|
||||
</svelte:fragment>
|
||||
</PinkEmpty>
|
||||
</Card.Base>
|
||||
|
||||
@@ -1,45 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
EmptySearch,
|
||||
SearchQuery,
|
||||
PaginationWithLimit,
|
||||
ViewSelector,
|
||||
EmptyFilter
|
||||
} from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import { EmptySearch, PaginationWithLimit, EmptyFilter } from '$lib/components';
|
||||
import { Container, ResponsiveContainerHeader } from '$lib/layout';
|
||||
import { columns } from './store';
|
||||
import { Filters, hasPageQueries } from '$lib/components/filters';
|
||||
import { hasPageQueries } from '$lib/components/filters';
|
||||
import CreateProviderDropdown from './createProviderDropdown.svelte';
|
||||
import Table from './table.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { canWriteProviders } from '$lib/stores/roles';
|
||||
import { Card, Layout, Empty, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { Card, Empty, Icon } from '@appwrite.io/pink-svelte';
|
||||
import { View } from '$lib/helpers/load';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data;
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<SearchQuery placeholder="Search providers" />
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
|
||||
<Filters query={data.query} {columns} analyticsSource="messaging_providers" />
|
||||
<ViewSelector ui="new" view={View.Table} {columns} hideView />
|
||||
{#if $canWriteProviders}
|
||||
<CreateProviderDropdown let:toggle>
|
||||
<Button on:click={toggle} event="create_provider">
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create provider
|
||||
</Button>
|
||||
</CreateProviderDropdown>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<ResponsiveContainerHeader
|
||||
{columns}
|
||||
view={View.Table}
|
||||
hideView
|
||||
hasFilters
|
||||
hasSearch
|
||||
analyticsSource="messaging_providers"
|
||||
searchPlaceholder="Search by name or ID">
|
||||
{#if $canWriteProviders}
|
||||
<CreateProviderDropdown let:toggle>
|
||||
<Button on:click={toggle} event="create_provider">
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create provider
|
||||
</Button>
|
||||
</CreateProviderDropdown>
|
||||
{/if}
|
||||
</ResponsiveContainerHeader>
|
||||
|
||||
{#if data.providers.total}
|
||||
<Table {data} />
|
||||
@@ -52,11 +47,7 @@
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="providers" />
|
||||
{:else if data.search && data.search !== 'empty'}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no providers that match your search.</p>
|
||||
</div>
|
||||
<EmptySearch target="providers" search={data.search}>
|
||||
<Button
|
||||
secondary
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/messaging/providers`}>
|
||||
@@ -68,7 +59,7 @@
|
||||
<Empty
|
||||
title="Create your first provider"
|
||||
description="Need a hand? Learn more in our documentation.">
|
||||
<slot name="actions" slot="actions">
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/providers"
|
||||
@@ -82,7 +73,7 @@
|
||||
</Button>
|
||||
</CreateProviderDropdown>
|
||||
{/if}
|
||||
</slot>
|
||||
</svelte:fragment>
|
||||
</Empty>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
import { providers } from './store';
|
||||
import CreateMember from '$routes/(console)/organization-[organization]/createMember.svelte';
|
||||
|
||||
let formRef: Form;
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
let response: Models.Provider;
|
||||
@@ -185,7 +187,7 @@
|
||||
</script>
|
||||
|
||||
<Wizard title="Create provider" columnSize="l">
|
||||
<Form onSubmit={create} isModal={false}>
|
||||
<Form bind:this={formRef} onSubmit={create} isModal={false}>
|
||||
<Layout.Stack gap="xxl">
|
||||
<Fieldset legend="Provider">
|
||||
<Provider />
|
||||
@@ -193,9 +195,6 @@
|
||||
<Fieldset legend="Settings">
|
||||
<Settings />
|
||||
</Fieldset>
|
||||
<Layout.Stack justifyContent="flex-end" direction="row">
|
||||
<Button submit>Create</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
@@ -227,7 +226,9 @@
|
||||
hasDivider
|
||||
href={`https://appwrite.io/docs/products/messaging/${$provider}`}
|
||||
title="Read the guide in the docs"
|
||||
icon={IconBookOpen} />
|
||||
icon={IconBookOpen}
|
||||
target="_blank"
|
||||
rel="noreferrer" />
|
||||
<ActionList.Item.Button
|
||||
on:click={() => {
|
||||
$newMemberModal = true;
|
||||
@@ -241,4 +242,9 @@
|
||||
{#if $newMemberModal}
|
||||
<CreateMember bind:showCreate={$newMemberModal} />
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack justifyContent="flex-end" direction="row">
|
||||
<Button on:click={() => formRef.triggerSubmit()}>Create</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Wizard>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
import { ActionMenu, Popover } from '@appwrite.io/pink-svelte';
|
||||
</script>
|
||||
|
||||
<Popover let:toggle padding="none" placement="bottom-end">
|
||||
<Popover let:toggle padding="none" placement="bottom-start">
|
||||
<slot {toggle} />
|
||||
<ActionMenu.Root slot="tooltip">
|
||||
{#each Object.entries(providers) as [type, option]}
|
||||
|
||||
+8
-13
@@ -1,30 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { ImagePreview } from '$lib/components';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let lines = [];
|
||||
export let image: { src: { dark: string; light: string }; alt: string } | null = null;
|
||||
</script>
|
||||
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<Layout.Stack direction="column" gap="m" style="max-inline-size: 260px;">
|
||||
{#each lines as line}
|
||||
<p>{@html line}</p>
|
||||
<p class="popover-content">{@html line}</p>
|
||||
{/each}
|
||||
|
||||
{#if image}
|
||||
<ImagePreview darkSrc={image.src.dark} lightSrc={image.src.light} alt={image.alt} />
|
||||
<Layout.Stack>
|
||||
<ImagePreview darkSrc={image.src.dark} lightSrc={image.src.light} alt={image.alt} />
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
|
||||
<style>
|
||||
div > p {
|
||||
color: hsl(var(--color-neutral-50));
|
||||
|
||||
&:first-child {
|
||||
color: hsl(var(--color-neutral-70));
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) div > p {
|
||||
:global(.theme-dark) .popover-content {
|
||||
color: hsl(var(--color-neutral-20));
|
||||
|
||||
&:first-child {
|
||||
|
||||
@@ -97,11 +97,7 @@
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="topics" />
|
||||
{:else if data.search}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no topics that match your search.</p>
|
||||
</div>
|
||||
<EmptySearch target="topics" search={data.search}>
|
||||
<Button
|
||||
secondary
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/messaging/topics`}>
|
||||
|
||||
+11
-11
@@ -25,8 +25,9 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { base } from '$app/paths';
|
||||
import { Icon, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Link } from '$lib/elements';
|
||||
|
||||
export let data;
|
||||
let showAdd = false;
|
||||
@@ -71,9 +72,7 @@
|
||||
await Promise.all(promises);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Added ${targetIds.length} subscriber${
|
||||
targetIds.length > 1 ? 's' : ''
|
||||
} to topic`
|
||||
message: `${targetIds.length} subscriber${targetIds.length !== 1 ? 's' : ''} have been added`
|
||||
});
|
||||
trackEvent(Submit.MessagingTopicSubscriberAdd);
|
||||
await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
|
||||
@@ -141,13 +140,14 @@
|
||||
bind:targetsById={$targetsById}
|
||||
on:update={addTargets}>
|
||||
<svelte:fragment slot="description">
|
||||
<p class="text">
|
||||
Add subscribers to this topic by selecting the targets for directing messages. <a
|
||||
<Typography.Text>
|
||||
Add subscribers to this topic by selecting the targets for directing messages.{' '}
|
||||
<Link
|
||||
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more about subscribers</a
|
||||
>.
|
||||
</p>
|
||||
external>
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
</UserTargetsModal>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { MessagingProviderType, type Models, Query } from '@appwrite.io/console';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { getTotal } from './wizard/store';
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
@@ -17,21 +16,42 @@
|
||||
import { getProviderText } from './helper';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export let providerType: MessagingProviderType;
|
||||
export let show: boolean;
|
||||
export let topicsById: Record<string, Models.Topic>;
|
||||
export let title = 'Select topics';
|
||||
let {
|
||||
providerType,
|
||||
show = $bindable(),
|
||||
topicsById = $bindable(),
|
||||
title = 'Select topics'
|
||||
} = $props<{
|
||||
providerType: MessagingProviderType;
|
||||
show: boolean;
|
||||
topicsById: Record<string, Models.Topic>;
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let search = '';
|
||||
let offset = 0;
|
||||
let totalResults = 0;
|
||||
let topicResultsById: Record<string, Models.Topic> = {}; // use a hash map so we can quickly look up a user by id
|
||||
let selected: Record<string, Models.Topic> = {};
|
||||
let hasSelection = false;
|
||||
let search = $state('');
|
||||
let offset = $state(0);
|
||||
let totalResults = $state(0);
|
||||
let topicResultsById = $state<Record<string, Models.Topic>>({});
|
||||
let selected = $state<Record<string, Models.Topic>>({});
|
||||
let emptyTopicsExists = $state(false);
|
||||
|
||||
let emptyTopicsExists = false;
|
||||
let previousSearch = $state('');
|
||||
let wasOpen = $state(false);
|
||||
|
||||
function getTopicTotal(topic: Models.Topic): number {
|
||||
switch (providerType) {
|
||||
case MessagingProviderType.Email:
|
||||
return topic.emailTotal;
|
||||
case MessagingProviderType.Sms:
|
||||
return topic.smsTotal;
|
||||
case MessagingProviderType.Push:
|
||||
return topic.pushTotal;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
offset = 0;
|
||||
@@ -87,31 +107,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (show) {
|
||||
request();
|
||||
}
|
||||
$: if (offset !== null) {
|
||||
request();
|
||||
}
|
||||
$: if (search !== null) {
|
||||
offset = 0;
|
||||
request();
|
||||
}
|
||||
$effect(() => {
|
||||
if (search !== previousSearch) {
|
||||
previousSearch = search;
|
||||
offset = 0;
|
||||
}
|
||||
});
|
||||
|
||||
$: selectedSize = Object.keys(selected).length;
|
||||
$effect(() => {
|
||||
if (!show) return;
|
||||
offset ?? null;
|
||||
search ?? null;
|
||||
request();
|
||||
});
|
||||
|
||||
$: hasSelection = selectedSize > 0;
|
||||
$effect(() => {
|
||||
if (show && !wasOpen) {
|
||||
selected = topicsById;
|
||||
}
|
||||
wasOpen = show;
|
||||
});
|
||||
|
||||
$: if (show) {
|
||||
selected = topicsById;
|
||||
}
|
||||
let selectedSize = $derived(Object.keys(selected).length);
|
||||
let hasSelection = $derived(selectedSize > 0);
|
||||
|
||||
let topicSelectionStates = $derived(
|
||||
Object.fromEntries(Object.keys(topicResultsById).map((id) => [id, !!selected[id]]))
|
||||
);
|
||||
</script>
|
||||
|
||||
<Modal {title} bind:show onSubmit={submit} on:close={reset}>
|
||||
<Typography.Text>
|
||||
Select existing topics you want to send this message to its targets. The message will be
|
||||
sent only to {getProviderText(providerType)} targets.
|
||||
</Typography.Text>
|
||||
<svelte:fragment slot="description">
|
||||
<Typography.Text>
|
||||
Select existing topics you want to send this message to its targets. The message will be
|
||||
sent only to {getProviderText(providerType)} targets.
|
||||
</Typography.Text>
|
||||
</svelte:fragment>
|
||||
<Layout.Stack>
|
||||
<InputSearch
|
||||
autofocus
|
||||
@@ -128,11 +159,11 @@
|
||||
id={topicId}
|
||||
label={topic.name}
|
||||
disabled={!!topicsById[topicId]}
|
||||
checked={!!selected[topicId]}
|
||||
checked={topicSelectionStates[topicId] || false}
|
||||
on:change={(event) => onTopicSelection(event, topic)}>
|
||||
</Selector.Checkbox>
|
||||
<span>
|
||||
({getTotal(topic)} targets)
|
||||
({getTopicTotal(topic)} targets)
|
||||
</span>
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
@@ -174,7 +205,7 @@
|
||||
<Badge variant="secondary" content={selectedSize.toString()} />
|
||||
<span>Topics selected</span>
|
||||
</Layout.Stack>
|
||||
<Button submit disabled={!hasSelection}>Add</Button>
|
||||
<Button submit disabled={!hasSelection}>Create</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
<Badge variant="secondary" content={selectedUsers.toString()} />
|
||||
<span>Users selected</span>
|
||||
</Layout.Stack>
|
||||
<Button submit disabled={!hasSelection}>Add</Button>
|
||||
<Button submit disabled={!hasSelection}>Create</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -39,7 +39,6 @@ export const load: PageLoad = async ({ parent, depends, url }) => {
|
||||
} finally {
|
||||
redirect(303, resolve('/'));
|
||||
}
|
||||
} else {
|
||||
redirect(303, resolve('/'));
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user