mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #217 from appwrite/feat-display-attribute-types
feat: display attribute types on input fields
This commit is contained in:
@@ -1,17 +1,24 @@
|
||||
import type { Action } from 'svelte/action';
|
||||
import type { Props } from 'tippy.js';
|
||||
import type { Props as TippyProps } from 'tippy.js';
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
type Props = TippyProps & {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const tooltip: Action<HTMLElement, Partial<Props>> = (node, config) => {
|
||||
const instance = tippy(node, config);
|
||||
if (config.disabled) instance.disable();
|
||||
|
||||
return {
|
||||
update({ content }) {
|
||||
update({ content, disabled }) {
|
||||
if (content !== instance.props.content) {
|
||||
instance.setProps({
|
||||
content
|
||||
});
|
||||
}
|
||||
|
||||
disabled ? instance.disable() : instance.enable();
|
||||
},
|
||||
destroy() {
|
||||
instance.destroy();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { FormItem, Helper } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let value = false;
|
||||
@@ -27,6 +28,8 @@
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
@@ -39,6 +40,8 @@
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { FormItem, Helper } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let value: number = null;
|
||||
@@ -51,7 +52,9 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<label class:u-hide={!showLabel || !label} class="label" for={id}>{label}</label>
|
||||
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let value: string | number | boolean;
|
||||
export let placeholder = '';
|
||||
@@ -33,6 +34,8 @@
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>{label}</label>
|
||||
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
|
||||
|
||||
<div class="select">
|
||||
<select
|
||||
{id}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { FormItem, Helper } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
@@ -42,8 +43,8 @@
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<label class:u-hide={!showLabel} class="label" for={id}
|
||||
>{label}
|
||||
<label class:u-hide={!showLabel} class="label" for={id}>
|
||||
{label}
|
||||
{#if tooltip}
|
||||
<span
|
||||
class="icon-info"
|
||||
@@ -53,6 +54,8 @@
|
||||
}} />
|
||||
{/if}
|
||||
</label>
|
||||
<span class:u-hide={!showLabel || !optionalText} class="optional">{optionalText}</span>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
|
||||
@@ -50,7 +50,6 @@ export function at<T>(array: readonly T[] | [], index: number): T | undefined {
|
||||
|
||||
/**
|
||||
* Get last item
|
||||
* @date 12/12/2022 - 12:08:03 PM
|
||||
*
|
||||
* @export
|
||||
* @template T
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Strict typed `Object.entries`
|
||||
* Extracted from https://github.com/antfu/utils
|
||||
*
|
||||
* @category Object
|
||||
*/
|
||||
export function objectEntries<T extends object>(obj: T) {
|
||||
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>;
|
||||
}
|
||||
|
||||
type KeyTypesMap<T> = Record<keyof T, Array<string> | ((v: unknown) => boolean) | string>;
|
||||
/**
|
||||
* Checks if the given value is an object of type T, given a map of keys and
|
||||
* their types/validators.
|
||||
*
|
||||
* @category Object
|
||||
*
|
||||
* @template T
|
||||
* @param {unknown} object
|
||||
* @param {KeyTypesMap} keyTypesMap
|
||||
* @returns {value is T}
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* type ExampleType = {
|
||||
* a: string;
|
||||
* b: number;
|
||||
* c: string | number;
|
||||
* d: string[]
|
||||
* e: {
|
||||
* f: string;
|
||||
* g: number;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* export const isExampleType = (value: unknown): value is ExampleType => {
|
||||
* return isObjectType(value, {
|
||||
* a: 'string',
|
||||
* b: 'number',
|
||||
* c: ['string', 'number'],
|
||||
* d: (v) => Array.isArray(v) && v.every((v) => typeof v === 'string'),
|
||||
* e: (v) => isObjectType(v, {
|
||||
* f: 'string',
|
||||
* g: 'number',
|
||||
* }),
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export const isObjectType = <T>(
|
||||
object: unknown,
|
||||
keyTypesMap: Partial<KeyTypesMap<T>>
|
||||
): object is T => {
|
||||
if (typeof object !== 'object' || object === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [key, check] of objectEntries(keyTypesMap)) {
|
||||
const value = (object as Record<keyof T, unknown>)[key];
|
||||
|
||||
if (typeof check === 'function') {
|
||||
if (!check(value)) {
|
||||
return false;
|
||||
}
|
||||
} else if (typeof check === 'string') {
|
||||
if (typeof value !== check) {
|
||||
return false;
|
||||
}
|
||||
} else if (!check.includes(typeof value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Capitalizes the first letter of a string
|
||||
*
|
||||
* @export
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
export function capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
+47
-1
@@ -19,6 +19,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { PAGE_LIMIT } from '$lib/constants';
|
||||
import CreateAttribute from '../createAttribute.svelte';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
export let data: PageData;
|
||||
let showCreateAttribute = false;
|
||||
@@ -33,6 +34,44 @@
|
||||
title: attribute.key
|
||||
}))
|
||||
];
|
||||
|
||||
function formatArray(array: any[]) {
|
||||
if (array.length === 0) return '[ ]';
|
||||
|
||||
let formattedFields: string[] = [];
|
||||
for (const item of array) {
|
||||
if (typeof item === 'string') {
|
||||
formattedFields.push(`"${item}"`);
|
||||
} else {
|
||||
formattedFields.push(`${item}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `[${formattedFields.join(', ')}]`;
|
||||
}
|
||||
|
||||
function formatColumn(column: any) {
|
||||
let formattedColumn: string;
|
||||
|
||||
if (typeof column === 'string') {
|
||||
formattedColumn = column;
|
||||
} else if (Array.isArray(column)) {
|
||||
formattedColumn = formatArray(column);
|
||||
} else if (!column) {
|
||||
formattedColumn = 'n/a';
|
||||
} else {
|
||||
formattedColumn = `${column}`;
|
||||
}
|
||||
|
||||
return {
|
||||
value:
|
||||
formattedColumn.length > 20
|
||||
? `${formattedColumn.slice(0, 20)}...`
|
||||
: formattedColumn,
|
||||
truncated: formattedColumn.length > 20,
|
||||
whole: formattedColumn
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@@ -70,8 +109,15 @@
|
||||
</Copy>
|
||||
</TableCell>
|
||||
{#each columns as column}
|
||||
{@const formatted = formatColumn(document[column.key])}
|
||||
<TableCell>
|
||||
{document[column.key] ?? 'n/a'}
|
||||
<div
|
||||
use:tooltip={{
|
||||
content: formatted.whole,
|
||||
disabled: !formatted.truncated
|
||||
}}>
|
||||
{formatted.value}
|
||||
</div>
|
||||
</TableCell>
|
||||
{/each}
|
||||
</TableRowLink>
|
||||
|
||||
+3
@@ -9,6 +9,7 @@
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
export let value: string | number | boolean;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let attribute:
|
||||
| Models.AttributeBoolean
|
||||
| Models.AttributeEmail
|
||||
@@ -43,6 +44,7 @@
|
||||
{id}
|
||||
{label}
|
||||
{attribute}
|
||||
{optionalText}
|
||||
bind:value />
|
||||
{:else}
|
||||
<svelte:component
|
||||
@@ -50,6 +52,7 @@
|
||||
{id}
|
||||
{label}
|
||||
{attribute}
|
||||
{optionalText}
|
||||
bind:value />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
+36
-19
@@ -3,7 +3,8 @@
|
||||
import { FormList } from '$lib/elements/forms';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import Pill from '$lib/elements/pill.svelte';
|
||||
import type { Attributes } from '../store';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { isAttributeEnum, type Attributes } from '../store';
|
||||
import Attribute from './attribute.svelte';
|
||||
|
||||
export let attributes: Attributes[] = [];
|
||||
@@ -26,6 +27,11 @@
|
||||
[key]: [...formValues[key], null]
|
||||
};
|
||||
}
|
||||
|
||||
function getAttributeType(attribute: Attributes) {
|
||||
if (isAttributeEnum(attribute)) return 'Enum';
|
||||
return `${capitalize(attribute.type)}${attribute.array ? '[]' : ''}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if attributes.length}
|
||||
@@ -35,7 +41,11 @@
|
||||
{#if attribute.array}
|
||||
{#if formValues[attribute.key].length === 0}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<span class="label u-margin-0">{label}</span>
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<span class="label u-margin-0">{label}</span>
|
||||
<span class="optional">{getAttributeType(attribute)}</span>
|
||||
</div>
|
||||
|
||||
<Button text noMargin on:click={() => addArrayItem(attribute.key)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text"> Add item</span>
|
||||
@@ -43,24 +53,30 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each [...formValues[attribute.key].keys()] as index}
|
||||
<li class="form-item is-multiple">
|
||||
<div class="form-item-part u-stretch">
|
||||
<Attribute
|
||||
{attribute}
|
||||
id={`${attribute.key}-${index}`}
|
||||
label={index === 0 ? label : ''}
|
||||
bind:value={formValues[attribute.key][index]} />
|
||||
</div>
|
||||
<div class="form-item-part u-cross-child-end">
|
||||
<Button text on:click={() => removeArrayItem(attribute.key, index)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
{#if formValues[attribute.key].length !== 0}
|
||||
<ul class="u-grid u-gap-8">
|
||||
{#each [...formValues[attribute.key].keys()] as index}
|
||||
<li class="form-item is-multiple">
|
||||
<div class="form-item-part u-stretch">
|
||||
<Attribute
|
||||
{attribute}
|
||||
id={`${attribute.key}-${index}`}
|
||||
optionalText={index === 0
|
||||
? getAttributeType(attribute)
|
||||
: undefined}
|
||||
label={index === 0 ? label : ''}
|
||||
bind:value={formValues[attribute.key][index]} />
|
||||
</div>
|
||||
<div class="form-item-part u-cross-child-end">
|
||||
<Button
|
||||
text
|
||||
on:click={() => removeArrayItem(attribute.key, index)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<Button text noMargin on:click={() => addArrayItem(attribute.key)}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text"> Add item</span>
|
||||
@@ -71,6 +87,7 @@
|
||||
<Attribute
|
||||
{attribute}
|
||||
{label}
|
||||
optionalText={getAttributeType(attribute)}
|
||||
id={attribute.key}
|
||||
bind:value={formValues[attribute.key]} />
|
||||
</FormList>
|
||||
|
||||
+3
-1
@@ -6,12 +6,14 @@
|
||||
export let label: string;
|
||||
export let value: boolean;
|
||||
export let attribute: Models.AttributeBoolean;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
{#if attribute?.array}
|
||||
<InputSelect
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
showLabel={!!label?.length}
|
||||
placeholder="Select a value"
|
||||
required={attribute.required}
|
||||
@@ -21,5 +23,5 @@
|
||||
]}
|
||||
bind:value />
|
||||
{:else}
|
||||
<InputCheckbox {id} {label} required={attribute.required} bind:value />
|
||||
<InputCheckbox {id} {label} {optionalText} required={attribute.required} bind:value />
|
||||
{/if}
|
||||
|
||||
+8
-1
@@ -6,6 +6,13 @@
|
||||
export let label: string;
|
||||
export let value: string;
|
||||
export let attribute: Models.AttributeDatetime;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<InputDateTime {id} {label} showLabel={!!label?.length} required={attribute.required} bind:value />
|
||||
<InputDateTime
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
showLabel={!!label?.length}
|
||||
required={attribute.required}
|
||||
bind:value />
|
||||
|
||||
+2
@@ -6,6 +6,7 @@
|
||||
export let label: string;
|
||||
export let value: string;
|
||||
export let attribute: Models.AttributeEnum;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
|
||||
$: options = attribute.elements.map((element) => {
|
||||
return {
|
||||
@@ -20,5 +21,6 @@
|
||||
{options}
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
placeholder="Select a value"
|
||||
showLabel={!!label?.length} />
|
||||
|
||||
+2
@@ -6,11 +6,13 @@
|
||||
export let label: string;
|
||||
export let value: number;
|
||||
export let attribute: Models.AttributeInteger;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<InputNumber
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
required={attribute.required}
|
||||
min={attribute.min}
|
||||
max={attribute.max}
|
||||
|
||||
+2
@@ -6,11 +6,13 @@
|
||||
export let label: string;
|
||||
export let value: string;
|
||||
export let attribute: Models.AttributeString;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<InputText
|
||||
{id}
|
||||
{label}
|
||||
{optionalText}
|
||||
showLabel={!!label?.length}
|
||||
required={attribute.required}
|
||||
maxlength={attribute.size}
|
||||
|
||||
+7
@@ -1,6 +1,7 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@aw-labs/appwrite-console';
|
||||
import { isObjectType } from '$lib/helpers/object';
|
||||
|
||||
export type Attributes =
|
||||
| Models.AttributeBoolean
|
||||
@@ -12,6 +13,12 @@ export type Attributes =
|
||||
| Models.AttributeString
|
||||
| Models.AttributeUrl;
|
||||
|
||||
export function isAttributeEnum(attribute: unknown): attribute is Models.AttributeEnum {
|
||||
return isObjectType<Models.AttributeEnum>(attribute, {
|
||||
format: (v) => v === 'enum'
|
||||
});
|
||||
}
|
||||
|
||||
type Collection = Omit<Models.Collection, 'attributes'> & {
|
||||
attributes: Array<Attributes>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { isObjectType, objectEntries } from '$lib/helpers/object';
|
||||
|
||||
test('objectEntries', () => {
|
||||
const object = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3
|
||||
};
|
||||
|
||||
expect(objectEntries(object)).toEqual(Object.entries(object));
|
||||
});
|
||||
|
||||
test('isObjectType', () => {
|
||||
type TestType = {
|
||||
a: string;
|
||||
b: number;
|
||||
c: string | number;
|
||||
d: string[];
|
||||
e: {
|
||||
f: string;
|
||||
g: number;
|
||||
};
|
||||
h: undefined | null | string;
|
||||
};
|
||||
|
||||
const valid_type = {
|
||||
a: '1',
|
||||
b: 1,
|
||||
c: '1',
|
||||
d: ['1', '2'],
|
||||
e: {
|
||||
f: '1',
|
||||
g: 1
|
||||
},
|
||||
h: null
|
||||
};
|
||||
|
||||
const invalid_type = {
|
||||
a: 1,
|
||||
b: '1',
|
||||
c: true
|
||||
};
|
||||
|
||||
const invalid_type_2 = {
|
||||
...valid_type,
|
||||
d: [1, '2']
|
||||
};
|
||||
|
||||
const invalid_type_3 = {
|
||||
...valid_type,
|
||||
c: null
|
||||
};
|
||||
|
||||
const isTestType = (value: unknown): value is TestType => {
|
||||
return isObjectType(value, {
|
||||
a: 'string',
|
||||
b: 'number',
|
||||
c: ['string', 'number'],
|
||||
d: (v) => Array.isArray(v) && v.every((v) => typeof v === 'string'),
|
||||
e: (v) =>
|
||||
isObjectType(v, {
|
||||
f: 'string',
|
||||
g: 'number'
|
||||
}),
|
||||
h: (v) => v === null || v === undefined || typeof v === 'string'
|
||||
});
|
||||
};
|
||||
|
||||
expect(isTestType(valid_type)).toBe(true);
|
||||
expect(isTestType(invalid_type)).toBe(false);
|
||||
expect(isTestType(invalid_type_2)).toBe(false);
|
||||
expect(isTestType(invalid_type_3)).toBe(false);
|
||||
expect(isTestType('12331')).toBe(false);
|
||||
});
|
||||
Reference in New Issue
Block a user