+
{#if tables?.total}
{#each sortedTables as table, index}
@@ -147,25 +147,15 @@
-
+
@@ -177,7 +167,7 @@
{@const href = `${base}/project-${region}-${project}/databases/database-${databaseId}/${action.href}`}
-
+
@@ -191,7 +181,7 @@
{/each}
-
+
{:else if data?.database?.name && !isMainDatabaseScreen}
@@ -271,11 +261,15 @@
overflow-x: hidden;
min-height: 0;
margin-bottom: auto;
- padding-bottom: 16px;
- scrollbar-width: thin;
- scrollbar-color: var(--border-neutral, #ededf0) transparent;
color: var(--fgcolor-neutral-secondary, #56565c);
+ /* hide scrollbars */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ // scrollbar-width: thin;
+ // scrollbar-color: var(--border-neutral, #ededf0) transparent;
+
&::-webkit-scrollbar {
width: 4px;
height: 4px;
@@ -304,6 +298,10 @@
font-size: var(--font-size-sm);
color: var(--fgcolor-neutral-secondary);
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
&:not(.bottom-nav)::before {
content: '';
right: 99%;
@@ -335,12 +333,16 @@
position: relative;
padding-inline-end: 0.5rem;
margin-inline-start: 0.5rem;
- }
- li:hover {
- color: var(--fgcolor-neutral-primary);
- border-radius: var(--border-radius-s, 6px);
- background: var(--bgcolor-neutral-secondary);
+ &:hover {
+ color: var(--fgcolor-neutral-primary);
+ border-radius: var(--border-radius-s, 6px);
+ background: var(--bgcolor-neutral-secondary);
+ }
+
+ &.bottom-nav-item:hover {
+ margin-inline-end: 1.25rem;
+ }
}
.table-name {
@@ -382,8 +384,16 @@
line-height: 150%; /* 21px */
}
+ .bottom-nav-container {
+ right: 0;
+ bottom: 0;
+ left: 1.25rem;
+ position: absolute;
+ padding-block-end: 1rem;
+ }
+
.action-menu-divider {
- margin-inline: -1.2rem;
padding-block-end: 0.25rem;
+ margin-inline-start: -1.25rem;
}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte
index fcc995b71..19aa513a5 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte
@@ -64,6 +64,9 @@
import { chunks } from '$lib/helpers/array';
import { Submit, trackEvent } from '$lib/actions/analytics';
+ import IndexesSuggestions from '../(suggestions)/indexes.svelte';
+ import { showIndexesSuggestions, tableColumnSuggestions } from '../(suggestions)';
+
let editRow: EditRow;
let editRelatedRow: EditRelatedRow;
let editRowPermissions: EditRowPermissions;
@@ -89,7 +92,15 @@
response.events.includes('databases.*.tables.*.columns.*') ||
response.events.includes('databases.*.tables.*.indexes.*')
) {
- if (!isWaterfallFromFaker) {
+ // don't invalidate when -
+ // 1. from faker
+ // 2. ai columns creation
+ // 3. ai indexes creation
+ if (
+ !isWaterfallFromFaker &&
+ !$showIndexesSuggestions &&
+ !$tableColumnSuggestions.table
+ ) {
invalidate(Dependencies.TABLE);
}
}
@@ -442,7 +453,7 @@
title="Row permissions"
bind:show={$rowPermissionSheet.show}
submit={{
- text: 'Create',
+ text: 'Update',
disabled: editRowPermissions?.disableSubmit(),
onClick: async () => editRowPermissions?.updatePermissions()
}}>
@@ -470,3 +481,5 @@
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte
index b6d825c4f..df77376fa 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte
@@ -6,7 +6,7 @@
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
- import { Icon, Layout, Divider } from '@appwrite.io/pink-svelte';
+ import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import {
table,
@@ -31,6 +31,7 @@
import EmptySheet from './layout/emptySheet.svelte';
import CreateRow from './rows/create.svelte';
import { onDestroy } from 'svelte';
+ import { isCloud } from '$lib/system';
import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)';
export let data: PageData;
@@ -75,6 +76,12 @@
$: hasColumns = !!$table.columns.length;
$: hasValidColumns = $table?.columns?.some((col) => col.status === 'available');
+ $: canShowSuggestionsSheet =
+ // enabled, has table details
+ // and it matches current table
+ $tableColumnSuggestions.enabled &&
+ $tableColumnSuggestions.table &&
+ $tableColumnSuggestions.table.id === page.params.table;
async function onSelect(file: Models.File, localFile = false) {
$isCsvImportInProgress = true;
@@ -114,21 +121,31 @@
-
+
+
+
+
-
+ Columns
+
+
+
+
+
+ Filters
+
{/if}
- {:else if $tableColumnSuggestions.enabled && $tableColumnSuggestions.table && $tableColumnSuggestions.table.id === page.params.table}
+ {:else if isCloud && canShowSuggestionsSheet}
{:else}
{
const baseAttrs = [
@@ -104,8 +113,11 @@
let selectedColumn: Columns = $state(null);
let columnIndexMap: Record = $state({});
- let columnsOrder = $state([]);
+ let columnsOrder = $state([]);
+ let columnsWidth = $state(null);
+
const tableId = page.params.table;
+ const organizationId = data.organization.$id ?? data.project.teamId;
let showEdit = $state(false);
let editColumn: EditColumn;
@@ -126,6 +138,7 @@
onMount(() => {
columnsOrder = preferences.getColumnOrder(tableId);
+ columnsWidth = preferences.getColumnWidths(tableId + '#columns');
});
function getColumnStatusBadge(status: string): ComponentProps['type'] {
@@ -182,8 +195,74 @@
return `Type: ${formattedType}`;
}
+ function isSystemColumnKey(column: Columns) {
+ return column.key.startsWith('$');
+ }
+
+ function getColumnWidth(columnId: string, defaultWidth: number): number {
+ const savedWidth = columnsWidth?.[columnId];
+ if (!savedWidth) return defaultWidth;
+
+ return savedWidth.resized;
+ }
+
+ function saveColumnsWidth({ columnId, newWidth }: { columnId: string; newWidth: number }) {
+ const existing = columnsWidth?.[columnId];
+ const fixed = existing
+ ? typeof existing?.fixed === 'number'
+ ? existing.fixed
+ : existing?.fixed?.min
+ : newWidth;
+
+ columnsWidth = {
+ ...(columnsWidth ?? {}),
+ [columnId]: {
+ fixed,
+ resized: Math.ceil(newWidth)
+ }
+ };
+
+ saveColumnWidthsToPreferences({ columnId, newWidth, fixedWidth: fixed });
+ }
+
+ const saveColumnWidthsToPreferences = debounce(
+ (column: { columnId: string; newWidth: number; fixedWidth: number }) => {
+ if (!organizationId) return;
+
+ preferences.saveColumnWidths(organizationId, tableId + '#columns', {
+ [column.columnId]: {
+ fixed: column.fixedWidth,
+ resized: Math.ceil(column.newWidth)
+ }
+ });
+ },
+ 1000
+ );
+
onDestroy(() => ($showCreateColumnSheet.show = false));
+ const spreadsheetColumns = $derived([
+ {
+ id: 'key',
+ width: getColumnWidth('key', 300),
+ minimumWidth: 300,
+ resizable: true
+ },
+ {
+ id: 'indexed',
+ width: getColumnWidth('indexed', 150),
+ minimumWidth: 150,
+ resizable: true
+ },
+ {
+ id: 'default',
+ width: getColumnWidth('default', 200),
+ minimumWidth: 200,
+ resizable: true
+ },
+ { id: 'actions', width: 40, isAction: true, resizable: false }
+ ]);
+
$effect(() => {
if (!$showCreateIndexSheet.show && $showCreateIndexSheet.column) {
const columnKey = $showCreateIndexSheet.column;
@@ -224,14 +303,9 @@
height="100%"
emptyCells={emptyCellsCount}
bind:selectedRows={selectedColumns}
- columns={[
- // more size until we decide if we want a new column!
- { id: 'key', width: { min: $isSmallViewport ? 250 : 200 } },
- { id: 'indexed', width: { min: 150 } },
- { id: 'default', width: { min: 200 } },
- { id: 'actions', width: 40, isAction: true }
- ]}
- bottomActionClick={() => ($showCreateColumnSheet.show = true)}>
+ columns={spreadsheetColumns}
+ bottomActionClick={() => ($showCreateColumnSheet.show = true)}
+ on:columnsResize={(resize) => saveColumnsWidth(resize.detail)}>
Column name
Indexed
@@ -240,14 +314,23 @@
- {#each updatedColumnsForSheet as column, index}
+ {#each updatedColumnsForSheet as column, index (column.key)}
{@const option = columnOptions.find((option) => option.type === column.type)}
{@const isSelectable =
column['system'] || column.type === 'relationship' ? 'disabled' : true}
-
-
+
+
{#if isRelationship(column)}
{/if}
-
-
-
- {#if column.key === '$id' || column.key === '$sequence' || column.key === '$createdAt' || column.key === '$updatedAt'}
- {column['name']}
- {:else}
- {column.key} {column.array ? '[]' : undefined}
- {/if}
-
- {#if isString(column) && column.encrypt}
-
-
- Encrypted
-
+
+
+ {#if isSystemColumnKey(column)}
+ {column.key}
+ {:else}
+ {column.key}{column.array ? '[]' : undefined}
{/if}
-
+
+ {#if isString(column) && column.encrypt}
+
+
+ Encrypted
+
+ {/if}
+
+
{#if column.status !== 'available'}
{#if selectedColumn}
-
+
{:else if selectedColumns && selectedColumns.length}
{/if}
@@ -476,3 +565,13 @@
{#if showFailed}
{/if}
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte
index c1c3cbeae..827d77798 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/datetime.svelte
@@ -68,6 +68,7 @@
array: false,
...data
});
+
$: listen(data);
$: handleDefaultState($required || $array);
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte
index f201bb202..19d9f19d2 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte
@@ -61,7 +61,7 @@
await invalidate(Dependencies.TABLE);
showDelete = false;
- selectedColumn = [];
+ selectedColumn = Array.isArray(selectedColumn) ? [] : null;
} catch (e) {
error = e.message;
trackError(e, Submit.ColumnDelete);
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte
index cae8a573f..c393badec 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/float.svelte
@@ -85,14 +85,14 @@
label="Min"
placeholder="Enter size"
bind:value={data.min}
- step="any"
+ step={0.1}
required={editing} />
+ step={0.1} />
import { Container } from '$lib/layout';
- import { table } from '../store';
+ import { table, type ColumnsWidth } from '../store';
import Delete from './deleteIndex.svelte';
import CreateIndex from './createIndex.svelte';
import Overview from './overviewIndex.svelte';
@@ -8,6 +8,8 @@
import { Button } from '$lib/elements/forms';
import FailedModal from '../failedModal.svelte';
import { canWriteTables } from '$lib/stores/roles';
+ import { preferences } from '$lib/stores/preferences';
+ import { debounce } from '$lib/helpers/debounce';
import {
ActionMenu,
Badge,
@@ -26,7 +28,7 @@
IconPlus,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
- import { type ComponentProps, onDestroy } from 'svelte';
+ import { type ComponentProps, onDestroy, onMount } from 'svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
import EmptySheet from '../layout/emptySheet.svelte';
import SpreadsheetContainer from '../layout/spreadsheet.svelte';
@@ -34,6 +36,7 @@
import type { PageData } from './$types';
import { showCreateColumnSheet } from '../store';
import { isSmallViewport } from '$lib/stores/viewport';
+ import { page } from '$app/state';
let {
data
@@ -52,15 +55,49 @@
let showDelete = $state(false);
let showOverview = $state(false);
- let columns = $state([
- { id: 'key' },
- { id: 'type' },
- { id: 'columns' },
+ let columnsWidth = $state(null);
+
+ const tableId = page.params.table;
+ const organizationId = data.organization.$id ?? data.project.teamId;
+
+ const spreadsheetColumns = $derived([
+ {
+ id: 'key',
+ width: getColumnWidth('key', $isSmallViewport ? 250 : 200),
+ minimumWidth: $isSmallViewport ? 250 : 200,
+ resizable: true
+ },
+ {
+ id: 'type',
+ width: getColumnWidth('type', 120),
+ minimumWidth: 120,
+ resizable: true
+ },
+ {
+ id: 'columns',
+ width: getColumnWidth('columns', 200),
+ minimumWidth: 200,
+ resizable: true
+ },
// { id: 'orders' }, // design doesn't have orders atm
- { id: 'lengths' },
- { id: 'actions', width: 40, isAction: true }
+ {
+ id: 'lengths',
+ width: getColumnWidth('lengths', 180),
+ minimumWidth: 180,
+ resizable: true
+ },
+ {
+ id: 'actions',
+ width: 40,
+ isAction: true,
+ resizable: false
+ }
]);
+ onMount(() => {
+ columnsWidth = preferences.getColumnWidths(tableId + '#indexes');
+ });
+
function getColumnStatusBadge(status: string): ComponentProps['type'] {
switch (status) {
case 'processing':
@@ -74,6 +111,46 @@
}
}
+ function getColumnWidth(columnId: string, defaultWidth: number): number {
+ const savedWidth = columnsWidth?.[columnId];
+ if (!savedWidth) return defaultWidth;
+
+ return savedWidth.resized;
+ }
+
+ function saveColumnsWidth({ columnId, newWidth }: { columnId: string; newWidth: number }) {
+ const existing = columnsWidth?.[columnId];
+ const fixed = existing
+ ? typeof existing?.fixed === 'number'
+ ? existing.fixed
+ : existing?.fixed?.min
+ : newWidth;
+
+ columnsWidth = {
+ ...(columnsWidth ?? {}),
+ [columnId]: {
+ fixed,
+ resized: Math.ceil(newWidth)
+ }
+ };
+
+ saveColumnWidthsToPreferences({ columnId, newWidth, fixedWidth: fixed });
+ }
+
+ const saveColumnWidthsToPreferences = debounce(
+ (column: { columnId: string; newWidth: number; fixedWidth: number }) => {
+ if (!organizationId) return;
+
+ preferences.saveColumnWidths(organizationId, tableId + '#indexes', {
+ [column.columnId]: {
+ fixed: column.fixedWidth,
+ resized: Math.ceil(column.newWidth)
+ }
+ });
+ },
+ 1000
+ );
+
onDestroy(() => ($showCreateColumnSheet.show = false));
const emptyCellsLimit = $derived($isSmallViewport ? 14 : 17);
@@ -108,12 +185,13 @@
(showCreateIndex = true)}>
+ bottomActionClick={() => (showCreateIndex = true)}
+ on:columnsResize={(resize) => saveColumnsWidth(resize.detail)}>
Key
Type
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte
index 85c5b518a..88af9c257 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/createIndex.svelte
@@ -12,8 +12,9 @@
import { isRelationship, isSpatialType } from '../rows/store';
import { table, indexes } from '../store';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
- import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
+ import { IconCalendar, IconFingerPrint, IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
+ import { columnOptions as baseColumnOptions } from '../columns/store';
let {
showCreateIndex = $bindable(false),
@@ -37,7 +38,11 @@
}
return !isRelationship(column) && !isSpatialType(column); // keep non-relationship and non-spatial
})
- .map((column) => ({ value: column.key, label: column.key }))
+ .map((column) => ({
+ value: column.key,
+ label: column.key,
+ leadingIcon: baseColumnOptions.find((option) => option.type === column.type)?.icon
+ }))
);
let columnList = $state([{ value: '', order: '', length: null }]);
@@ -154,7 +159,7 @@
]
: undefined
});
- trackEvent(Submit.IndexCreate);
+ trackEvent(Submit.IndexCreate, { type: 'manual' });
showCreateIndex = false;
} catch (err) {
addNotification({
@@ -196,9 +201,17 @@
...(selectedType === IndexType.Spatial
? []
: [
- { value: '$id', label: '$id' },
- { value: '$createdAt', label: '$createdAt' },
- { value: '$updatedAt', label: '$updatedAt' }
+ { value: '$id', label: '$id', leadingIcon: IconFingerPrint },
+ {
+ value: '$createdAt',
+ label: '$createdAt',
+ leadingIcon: IconCalendar
+ },
+ {
+ value: '$updatedAt',
+ label: '$updatedAt',
+ leadingIcon: IconCalendar
+ }
]),
...columnOptions
]}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte
index 956bb1b6a..5916d398a 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/indexes/deleteIndex.svelte
@@ -34,8 +34,6 @@
})
)
);
- await invalidate(Dependencies.TABLE);
- showDelete = false;
addNotification({
type: 'success',
message:
@@ -43,7 +41,13 @@
? 'Index has been deleted'
: `${selectedKeys.length} indexes have been deleted`
});
+
trackEvent(Submit.IndexDelete);
+
+ await invalidate(Dependencies.TABLE);
+
+ showDelete = false;
+ selectedIndex = Array.isArray(selectedIndex) ? [] : null;
} catch (e) {
error = e.message;
trackError(e, Submit.IndexDelete);
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte
index bdc92fb75..8141cf0a7 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/emptySheet.svelte
@@ -325,7 +325,7 @@
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
- rgba(255, 255, 255, 0.855) 21%,
+ rgba(255, 255, 255, 0.86) 32.25%,
#ffffff 100%
);
z-index: 20;
@@ -347,10 +347,9 @@
:global(.theme-dark) .spreadsheet-fade-bottom {
background: linear-gradient(
180deg,
- rgba(25, 25, 28, 0.38) 13%,
- rgba(25, 25, 28, 0.7) 21%,
- rgba(25, 25, 28, 0.95) 38%,
- var(--bgcolor-neutral-default, #19191c) 100%
+ rgba(29, 29, 33, 0) 0%,
+ rgba(29, 29, 33, 0.86) 21%,
+ #1d1d21 100%
);
}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte
index 68b981d3a..c4d8041b2 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte
@@ -5,16 +5,20 @@
import { Button, Form } from '$lib/elements/forms';
import { isTabletViewport } from '$lib/stores/viewport';
import { Badge, Divider, Layout, Sheet, Tag, Typography } from '@appwrite.io/pink-svelte';
+ import type { HTMLAttributes } from 'svelte/elements';
+ import { beforeNavigate } from '$app/navigation';
let {
show = $bindable(false),
title,
closeOnBlur = false,
submit,
+ cancel,
children = null,
footer = null,
titleBadge = null,
- topAction = null
+ topAction = null,
+ ...restProps
}: {
show: boolean;
title: string;
@@ -36,17 +40,27 @@
onClick?: () => boolean | void | Promise;
}
| undefined;
+ cancel?:
+ | {
+ text?: string;
+ disabled?: boolean;
+ onClick?: () => void;
+ }
+ | undefined;
children?: Snippet;
footer?: Snippet | null;
- } = $props();
+ } & HTMLAttributes = $props();
let form: Form;
let submitting = $state(writable(false));
let copyText = $state(undefined);
+ beforeNavigate(() => {
+ show = false;
+ });
-
+
@@ -110,8 +124,19 @@
{#if footer}
{@render footer?.()}
{/if}
-
+
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte
index b1fa02ce4..8ac677c60 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte
@@ -58,7 +58,7 @@
},
{} as Record
),
- permissions: [],
+ permissions: existingData?.$permissions ?? [],
columns: availableColumns
};
}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
index ee2b81bb4..6923ba7e7 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
@@ -97,7 +97,7 @@
export let data: PageData;
export let showRowCreateSheet: {
show: boolean;
- row: Models.Row | null;
+ row: Partial | null;
};
$: rows = writable(data.rows);
@@ -297,7 +297,7 @@
switch (type) {
case 'string':
return IconText;
- case 'float':
+ case 'double':
case 'integer':
return IconHashtag;
case 'boolean':
@@ -543,7 +543,13 @@
}
if (action === 'duplicate-row') {
- showRowCreateSheet.row = row;
+ /**
+ * remove dates because
+ * console can override timestamps!
+ */
+ const { $createdAt, $updatedAt, ...rowWithoutDates } = row;
+
+ showRowCreateSheet.row = rowWithoutDates;
showRowCreateSheet.show = true;
}
@@ -1147,7 +1153,7 @@
- To change the selection edit the relationship settings.
+
{:else}
This action is irreversible.
@@ -1211,6 +1217,10 @@
padding-inline: 8px !important;
}
+ & :global(.input:has([type^='date'])) {
+ padding: 12px !important;
+ }
+
& :global(.input:focus-within) {
top: 0 !important;
}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts
index ef94e87b3..f5679dfb6 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts
@@ -33,9 +33,12 @@ export const tableColumns = writable([]);
export const isCsvImportInProgress = writable(false);
export const columnsOrder = writable([]);
-export const columnsWidth = writable<{
+
+export type ColumnsWidth = {
[columnId: string]: { fixed: number | { min: number }; resized: number };
-}>();
+};
+
+export const columnsWidth = writable();
type DatabaseSheetOptions = {
show: boolean;
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte
index 76334874f..28e5c2362 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table.svelte
@@ -6,7 +6,7 @@
import { Id } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
- import { toLocaleDateTime } from '$lib/helpers/date';
+ import DualTimeView from '$lib/components/dualTimeView.svelte';
import { addNotification } from '$lib/stores/notifications';
import { canWriteTables } from '$lib/stores/roles';
import { sdk } from '$lib/stores/sdk';
@@ -14,6 +14,7 @@
import type { PageData } from './$types';
import { tableViewColumns } from './store';
import Confirm from '$lib/components/confirm.svelte';
+ import { subNavigation } from '$lib/stores/database';
export let data: PageData;
const databaseId = page.params.database;
@@ -38,7 +39,8 @@
type: 'success',
message: `${selectedTables.length} table${selectedTables.length > 1 ? 's' : ''} deleted`
});
- invalidate(Dependencies.TABLES);
+ await invalidate(Dependencies.TABLES);
+ subNavigation.update();
} catch (error) {
addNotification({
type: 'error',
@@ -76,7 +78,7 @@
{:else if column.id === 'name'}
{table.name}
{:else}
- {toLocaleDateTime(table[column.id])}
+
{/if}
{/each}
diff --git a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+page.svelte b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+page.svelte
index ed9a8cc36..830ec0fc0 100644
--- a/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/functions/function-[function]/+page.svelte
@@ -86,10 +86,11 @@
{:else}
- (showAlert = false)}>
- Some configuration changes are not live yet. Your function is redeploying —
- changes will be applied once the build is complete.
-
+ (showAlert = false)}
+ title="Some configuration changes are not live yet. Your function is redeploying — changes will be applied once the build is complete." />
{/if}
{/if}
diff --git a/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte b/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte
index fcc732593..991d0ba6c 100644
--- a/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/messaging/+page.svelte
@@ -4,7 +4,7 @@
import { Empty, EmptyFilter, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { hasPageQueries } from '$lib/components/filters';
import { Button } from '$lib/elements/forms';
- import { toLocaleDateTime } from '$lib/helpers/date';
+ import DualTimeView from '$lib/components/dualTimeView.svelte';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import CreateMessageDropdown from './createMessageDropdown.svelte';
@@ -208,7 +208,7 @@
{#if !message[column.id]}
-
{:else}
- {toLocaleDateTime(message[column.id])}
+
{/if}
{:else}
{message[column.id]}
diff --git a/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/email.svelte b/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/email.svelte
index a7dbbf194..42d352d8e 100644
--- a/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/email.svelte
+++ b/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/email.svelte
@@ -23,7 +23,7 @@
let formComponent: Form;
let isSubmitting = writable(false);
let showCustomId = false;
- let id: string;
+ let id: string | null = null;
let subject: string;
let content: string;
let topics: string[];
diff --git a/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/push.svelte b/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/push.svelte
index 20978e8e9..415504dae 100644
--- a/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/push.svelte
+++ b/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/push.svelte
@@ -24,7 +24,7 @@
let formComponent: Form;
let isSubmitting = writable(false);
let showCustomId = false;
- let id: string;
+ let id: string | null = null;
let file: Models.File;
let data: Writable<[string, string][]> = writable([['', '']]);
let title: string;
diff --git a/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/sms.svelte b/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/sms.svelte
index 85909d99e..a1f88d7a3 100644
--- a/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/sms.svelte
+++ b/src/routes/(console)/project-[region]-[project]/messaging/create-[type]/(type)/sms.svelte
@@ -22,7 +22,7 @@
let formComponent: Form;
let isSubmitting = writable(false);
let showCustomId = false;
- let id: string;
+ let id: string | null = null;
let content: string;
let topics: string[];
let users: string[];
diff --git a/src/routes/(console)/project-[region]-[project]/messaging/message-[message]/updateTopics.svelte b/src/routes/(console)/project-[region]-[project]/messaging/message-[message]/updateTopics.svelte
index 2fb9b53c4..a96e1830b 100644
--- a/src/routes/(console)/project-[region]-[project]/messaging/message-[message]/updateTopics.svelte
+++ b/src/routes/(console)/project-[region]-[project]/messaging/message-[message]/updateTopics.svelte
@@ -1,6 +1,7 @@
diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte
index 31fc89017..76d4d5e1e 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte
@@ -10,7 +10,7 @@
import { total } from '$lib/layout/usage.svelte';
import { BillingPlan } from '$lib/constants.js';
import { base } from '$app/paths';
- import { formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
+ import { formatCurrency, formatNumberWithCommas, clampMin } from '$lib/helpers/numbers';
import { getCountryName } from '$lib/helpers/diallingCodes.js';
import { Accordion, Icon, Layout, Link, Table, Typography } from '@appwrite.io/pink-svelte';
import { IconChartSquareBar } from '@appwrite.io/pink-icons-svelte';
@@ -36,11 +36,11 @@
$: legendData = [
{
name: 'Reads',
- value: data.usage.databasesReads.reduce((sum, item) => sum + item.value, 0)
+ value: clampMin(data.usage.databasesReads.reduce((sum, item) => sum + item.value, 0))
},
{
name: 'Writes',
- value: data.usage.databasesWrites.reduce((sum, item) => sum + item.value, 0)
+ value: clampMin(data.usage.databasesWrites.reduce((sum, item) => sum + item.value, 0))
}
];
diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+page.svelte
index 60bfba3a4..2bdaa009e 100644
--- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/+page.svelte
@@ -89,9 +89,12 @@
{:else}
- (showAlert = false)}>
- Some configuration changes are not live yet. Your site is redeploying — changes
- will be applied once the build is complete.
+ (showAlert = false)}>
{/if}
{/if}
diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deleteDeploymentModal.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deleteDeploymentModal.svelte
index bad9f7676..7a266dd37 100644
--- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deleteDeploymentModal.svelte
+++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deleteDeploymentModal.svelte
@@ -22,7 +22,10 @@
siteId: selectedDeployment.resourceId,
deploymentId: selectedDeployment.$id
});
- await invalidate(Dependencies.SITE);
+ await Promise.all([
+ invalidate(Dependencies.DEPLOYMENTS),
+ invalidate(Dependencies.SITE)
+ ]);
if (page.url.href.includes(`deployment-${selectedDeployment.$id}`)) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/sites/site-${page.params.site}/deployments`
diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte
index 6a3fbb1e0..731cd4513 100644
--- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte
+++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte
@@ -9,6 +9,7 @@
import { UsageCard } from '$lib/components';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
+ import { clampMin } from '$lib/helpers/numbers';
const now = new Date();
const rangeOptions = [
@@ -84,7 +85,10 @@
metric.value = time !== '0s' ? time : '-';
} else {
- metric.value = usage[metric.id] ?? '-';
+ metric.value =
+ usage[metric.id] !== undefined
+ ? clampMin(usage[metric.id]).toString()
+ : '-';
}
return metric;
});
diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte
index 8158cd657..9bb700a5a 100644
--- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte
+++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/deployments/table.svelte
@@ -60,7 +60,10 @@
trackError(error, Submit.DeploymentDelete);
} finally {
selectedRows = [];
- invalidate(Dependencies.DEPLOYMENTS);
+ await Promise.all([
+ invalidate(Dependencies.DEPLOYMENTS),
+ invalidate(Dependencies.SITE)
+ ]);
}
}
diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/create/+page.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/create/+page.svelte
index 3bb100c47..824281105 100644
--- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/create/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/create/+page.svelte
@@ -92,11 +92,11 @@
+ confirmExit
+ title="Create file"
+ bind:showExitModal
+ href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/`}>
{/if}
+
Drag and drop files here or click to upload
+
{#if files}
{
diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/manageFileToken.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/manageFileToken.svelte
index 9a3e5e266..33a9d9f29 100644
--- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/manageFileToken.svelte
+++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/file-[file]/manageFileToken.svelte
@@ -70,15 +70,15 @@
{:else}
-
{#if fileToken}
{@const formattedDate = cleanFormattedDate(fileToken.$createdAt)}
Edit the expiry of the file token created on {formattedDate}.
{:else}
Create a file token to grant access to a file.
-
- Learn more.
+ Learn more.
{/if}
diff --git a/src/routes/(console)/verify-email/+page.svelte b/src/routes/(console)/verify-email/+page.svelte
new file mode 100644
index 000000000..c70b0e2d3
--- /dev/null
+++ b/src/routes/(console)/verify-email/+page.svelte
@@ -0,0 +1,93 @@
+
+
+
+ Verify Email - Appwrite
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/(console)/verify-email/+page.ts b/src/routes/(console)/verify-email/+page.ts
new file mode 100644
index 000000000..15ad453bc
--- /dev/null
+++ b/src/routes/(console)/verify-email/+page.ts
@@ -0,0 +1,45 @@
+import { resolve } from '$app/paths';
+import { redirect } from '@sveltejs/kit';
+import type { PageLoad } from './$types';
+import { Dependencies } from '$lib/constants';
+import { sdk } from '$lib/stores/sdk';
+import { addNotification } from '$lib/stores/notifications';
+import { VARS } from '$lib/system';
+
+export const load: PageLoad = async ({ parent, depends, url }) => {
+ if (!VARS.EMAIL_VERIFICATION) {
+ redirect(303, resolve('/'));
+ }
+
+ const { account } = await parent();
+ depends(Dependencies.ACCOUNT);
+
+ const user = url.searchParams.get('userId') ?? null;
+ const secret = url.searchParams.get('secret') ?? null;
+
+ if (account && account.emailVerification === true) {
+ redirect(303, resolve('/'));
+ } else if (user && secret) {
+ try {
+ await sdk.forConsole.account.updateVerification({
+ userId: user,
+ secret
+ });
+
+ addNotification({
+ type: 'success',
+ message: 'Email has been verified',
+ timeout: 10000 // 10 seconds max, because if succeeded, loads takes some time!
+ });
+ } catch (error) {
+ addNotification({
+ type: 'error',
+ message: error.message
+ });
+ } finally {
+ redirect(303, resolve('/'));
+ }
+ } else {
+ redirect(303, resolve('/'));
+ }
+};
diff --git a/src/routes/(console)/wizard/support/store.test.ts b/src/routes/(console)/wizard/support/store.test.ts
new file mode 100644
index 000000000..a8b836983
--- /dev/null
+++ b/src/routes/(console)/wizard/support/store.test.ts
@@ -0,0 +1,105 @@
+import { describe, expect, it, beforeEach, vi } from 'vitest';
+import { isSupportOnline } from './store';
+
+describe('isSupportOnline', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ // Helper function to create a mock date
+ const mockDate = (hour: number, day: number) => {
+ const mockDate = new Date();
+ vi.spyOn(mockDate, 'getUTCDay').mockReturnValue(day);
+ vi.spyOn(mockDate, 'getUTCHours').mockReturnValue(hour);
+ vi.spyOn(global, 'Date').mockImplementation(() => mockDate);
+ return mockDate;
+ };
+
+ describe('weekday support hours (Monday-Friday)', () => {
+ it('should be online at 16:00 UTC (Monday)', () => {
+ mockDate(16, 1); // Monday, 16:00 UTC
+ expect(isSupportOnline()).toBe(true);
+ });
+
+ it('should be online at 20:00 UTC (Tuesday)', () => {
+ mockDate(20, 2); // Tuesday, 20:00 UTC
+ expect(isSupportOnline()).toBe(true);
+ });
+
+ it('should be online at 23:59 UTC (Wednesday)', () => {
+ mockDate(23, 3); // Wednesday, 23:59 UTC
+ expect(isSupportOnline()).toBe(true);
+ });
+
+ it('should be online at 23:00 UTC (Thursday)', () => {
+ mockDate(23, 4); // Thursday, 23:00 UTC
+ expect(isSupportOnline()).toBe(true);
+ });
+
+ it('should be online at 22:00 UTC (Friday)', () => {
+ mockDate(22, 5); // Friday, 22:00 UTC
+ expect(isSupportOnline()).toBe(true);
+ });
+ });
+
+ describe('outside support hours on weekdays', () => {
+ it('should be offline at 15:59 UTC (Monday)', () => {
+ mockDate(15, 1); // Monday, 15:59 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+
+ it('should be offline at 00:00 UTC (Tuesday)', () => {
+ mockDate(0, 2); // Tuesday, 00:00 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+
+ it('should be offline at 10:00 UTC (Wednesday)', () => {
+ mockDate(10, 3); // Wednesday, 10:00 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+
+ it('should be offline at 14:00 UTC (Friday)', () => {
+ mockDate(14, 5); // Friday, 14:00 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+ });
+
+ describe('weekends', () => {
+ it('should be offline on Saturday regardless of hour', () => {
+ mockDate(18, 6); // Saturday, 18:00 UTC (in support hours for weekdays)
+ expect(isSupportOnline()).toBe(false);
+ });
+
+ it('should be offline on Sunday regardless of hour', () => {
+ mockDate(0, 0); // Sunday, 00:00 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+
+ it('should be offline on Saturday at midnight', () => {
+ mockDate(0, 6); // Saturday, 00:00 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+
+ it('should be offline on Sunday at 16:00', () => {
+ mockDate(16, 0); // Sunday, 16:00 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should handle boundary at 15:59 (just before support hours)', () => {
+ mockDate(15, 1); // Monday, 15:59 UTC
+ expect(isSupportOnline()).toBe(false);
+ });
+
+ it('should handle boundary at 16:00 (start of support hours)', () => {
+ mockDate(16, 1); // Monday, 16:00 UTC
+ expect(isSupportOnline()).toBe(true);
+ });
+
+ it('should handle late night hours on Friday', () => {
+ mockDate(23, 5); // Friday, 23:00 UTC
+ expect(isSupportOnline()).toBe(true);
+ });
+ });
+});
diff --git a/src/routes/(console)/wizard/support/store.ts b/src/routes/(console)/wizard/support/store.ts
index 535579f79..06f671b6a 100644
--- a/src/routes/(console)/wizard/support/store.ts
+++ b/src/routes/(console)/wizard/support/store.ts
@@ -20,15 +20,18 @@ export function isSupportOnline() {
const day = currentDate.getUTCDay();
const hour = currentDate.getUTCHours();
- if (day === 1 || day === 6) {
+ // Support is offline on weekends (Sunday = 0, Saturday = 6)
+ if (day === 0 || day === 6) {
return false;
}
- if (hour < 14) {
- return false;
+ // Support hours are 16:00 UTC to 23:59 UTC on weekdays
+ // This roughly covers 09:00 PT to 17:00 PT (depending on PST/PDT)
+ if (hour >= 16) {
+ return true;
}
- return true;
+ return false;
}
export const showSupportModal = writable(false);
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts
index 46b84cc98..5ee94b650 100644
--- a/src/routes/+layout.ts
+++ b/src/routes/+layout.ts
@@ -6,10 +6,10 @@ import { redirect } from '@sveltejs/kit';
import { Dependencies } from '$lib/constants';
import type { LayoutLoad } from './$types';
import { redirectTo } from './store';
-import { base } from '$app/paths';
+import { base, resolve } from '$app/paths';
import type { Account } from '$lib/stores/user';
import type { AppwriteException } from '@appwrite.io/console';
-import { isCloud } from '$lib/system';
+import { isCloud, VARS } from '$lib/system';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
export const ssr = false;
@@ -29,6 +29,15 @@ export const load: LayoutLoad = async ({ depends, url, route }) => {
}
if (account) {
+ if (isCloud && !account.emailVerification && VARS.EMAIL_VERIFICATION) {
+ const isConsoleRoute = route.id?.startsWith('/(console)');
+ const isVerifyEmailPage = url.pathname === resolve('/verify-email');
+
+ if (isConsoleRoute && !isVerifyEmailPage) {
+ redirect(303, resolve('/verify-email'));
+ }
+ }
+
return {
account: account,
organizations: !isCloud
diff --git a/static/logos/apple-touch-icon-120x120.png b/static/logos/apple-touch-icon-120x120.png
new file mode 100644
index 000000000..b5e9c5ea1
Binary files /dev/null and b/static/logos/apple-touch-icon-120x120.png differ
diff --git a/static/logos/apple-touch-icon-152x152.png b/static/logos/apple-touch-icon-152x152.png
new file mode 100644
index 000000000..93a40e457
Binary files /dev/null and b/static/logos/apple-touch-icon-152x152.png differ
diff --git a/static/logos/apple-touch-icon-167x167.png b/static/logos/apple-touch-icon-167x167.png
new file mode 100644
index 000000000..9083bef9d
Binary files /dev/null and b/static/logos/apple-touch-icon-167x167.png differ
diff --git a/static/logos/apple-touch-icon-180x180.png b/static/logos/apple-touch-icon-180x180.png
new file mode 100644
index 000000000..8e35bc723
Binary files /dev/null and b/static/logos/apple-touch-icon-180x180.png differ