Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item List Input component #1089

Merged
merged 26 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1e2b575
Add x-mark-icon
niwsa Mar 28, 2024
ee3865c
ItemList component
niwsa Mar 28, 2024
81bfe03
Move `ItemRow` to a separate file
niwsa Mar 29, 2024
e83fc6f
Merge branch 'main' into 1084-array-input-component
niwsa Mar 29, 2024
5b53783
Fix svelte ci failure
niwsa Mar 29, 2024
5f4f71e
Use <for> control flow
niwsa Mar 29, 2024
2b1b465
Refactor type and rename onChange handlers to handle...
niwsa Mar 29, 2024
bbf9e25
Tweaks for mitosis parsing
niwsa Mar 29, 2024
7ec269b
Use ItemList component
niwsa Mar 29, 2024
5539e26
Remove array parsing for redirectUrl
niwsa Mar 29, 2024
b91ac0d
Make ItemList handler generic for any field, fix around mitosis gotchas
niwsa Mar 29, 2024
4e9408f
Use ItemList component elsewhere
niwsa Mar 30, 2024
9c421b2
Update button text
niwsa Mar 30, 2024
b7fe437
Disable delete button for first item
niwsa Mar 30, 2024
803dd94
Validate duplicate entries
niwsa Mar 30, 2024
a59724a
Pass errorCallback
niwsa Mar 30, 2024
013527c
Label support, Display duplicate entry error inline, style tweaks, di…
niwsa Mar 31, 2024
b996135
Style error text
niwsa Apr 1, 2024
c51ad5a
Tweak prop name
niwsa Apr 1, 2024
dbd2902
Style fixes and use shared Button component
niwsa Apr 1, 2024
b41837c
Pass label, type and fix hint text
niwsa Apr 1, 2024
4449234
Style tweaks for delete button
niwsa Apr 1, 2024
25cfc4e
Warning triangle plus style tweaks for error message
niwsa Apr 1, 2024
917c02b
Adjust font-size
niwsa Apr 1, 2024
214dac3
Export `ItemList` for external use
niwsa Apr 1, 2024
8e16f85
Merge branch 'main' into 1084-array-input-component
niwsa Apr 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/shared/icons/XMarkIcon.lite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SVGProps } from '../types';

export default function XMarkIcon(props: { svgAttrs?: SVGProps }) {
return (
<svg fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' {...props.svgAttrs}>
<path stroke-linecap='round' stroke-linejoin='round' d='M6 18 18 6M6 6l12 12' />
</svg>
);
}
1 change: 1 addition & 0 deletions src/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as ClipboardButton } from './ClipboardButton/index.lite';
export { default as InputWithCopyButton } from './inputs/InputWithCopyButton/index.lite';
export { default as Themer } from './Themer/index.lite';
export { default as ItemList } from './inputs/ItemList/index.lite';
39 changes: 39 additions & 0 deletions src/shared/inputs/ItemList/ItemRow.lite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import XMarkIcon from '../../icons/XMarkIcon.lite';
import styles from '../index.module.css';
import itemStyles from './index.module.css';

type ItemRowProps = {
inputType: 'text' | 'url' | 'number' | 'password';
item: string;
index: number;
handleItemUpdate: (newItem: string, index: number) => void;
handleItemDelete: (index: number) => void;
handleBlur: (index: number) => void;
isDuplicateItem?: boolean;
disableDelete?: boolean;
disabled?: boolean;
classNames: { input: string };
};

export default function ItemRow(props: ItemRowProps) {
return (
<div class={itemStyles.row}>
<input
type={props.inputType || 'text'}
class={`${props.classNames.input} ${styles['input-sm']} ${itemStyles['input']}`}
name='item'
value={props.item}
onChange={(event) => props.handleItemUpdate(event.target.value, props.index)}
onBlur={(event) => props.handleBlur(props.index)}
required
disabled={props.disabled}
/>
<button
type='button'
onClick={(event) => props.handleItemDelete(props.index)}
disabled={props.disableDelete}>
<XMarkIcon svgAttrs={{ class: itemStyles['svg'] }} />
</button>
</div>
);
}
112 changes: 112 additions & 0 deletions src/shared/inputs/ItemList/index.lite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { For, Show, useStore } from '@builder.io/mitosis';
import ItemRow from './ItemRow.lite';
import styles from '../index.module.css';
import listStyles from './index.module.css';
import cssClassAssembler from '../../../sso/utils/cssClassAssembler';
import Button from '../../Button/index.lite';
import ExclamationTriangle from '../../icons/ExclamationTriangle.lite';

type ItemListProps = {
label: string;
inputType: 'text' | 'url' | 'number' | 'password';
classNames?: { label?: string; input?: string };
currentlist: string | string[];
fieldName: string;
handleItemListUpdate: (fieldName: string, newList: string[]) => void;
};

export default function ItemList(props: ItemListProps) {
const state = useStore({
duplicateEntryIndex: undefined as undefined | number,
get list() {
return Array.isArray(props.currentlist) ? props.currentlist : [props.currentlist];
},
addAnother: () => {
props.handleItemListUpdate(props.fieldName, [...state.list, '']);
},
handleItemUpdate: (newItem: string, index: number) => {
const newList = [...state.list];
newList[index] = newItem;
props.handleItemListUpdate(props.fieldName, newList);
},
checkDuplicates(index: number) {
const _item = state.list[index];
// search backwards
const _firstIndex = state.list.indexOf(_item);
if (_firstIndex !== index) {
state.duplicateEntryIndex = index;
return;
} else if (state.duplicateEntryIndex === index) {
state.duplicateEntryIndex = undefined;
}
// search forwards
const _nextIndex = state.list.slice(index + 1).indexOf(_item);
if (_nextIndex !== -1) {
state.duplicateEntryIndex = index;
} else if (state.duplicateEntryIndex === index) {
state.duplicateEntryIndex = undefined;
}
},
handleItemDelete: (index: number) => {
const _itemToDelete = state.list[index];
if (
state.duplicateEntryIndex !== undefined &&
(state.duplicateEntryIndex === index || _itemToDelete === state.list[state.duplicateEntryIndex])
) {
state.duplicateEntryIndex = undefined;
}
props.handleItemListUpdate(
props.fieldName,
state.list.filter((_, i) => i !== index)
);
},
get cssClass() {
return {
label: cssClassAssembler(props.classNames?.label, styles.label),
input: cssClassAssembler(props.classNames?.input, styles.input),
};
},
});

return (
<fieldset class={styles.fieldset}>
<legend class={state.cssClass.label}>{props.label}</legend>
<div class={listStyles.rowContainer}>
<For each={state.list}>
{(item, index) => (
<div key={index}>
<ItemRow
inputType={props.inputType}
item={item}
index={index}
isDuplicateItem={state.duplicateEntryIndex === index}
handleItemUpdate={state.handleItemUpdate}
handleItemDelete={state.handleItemDelete}
disableDelete={index === 0 && state.list.length === 1}
handleBlur={state.checkDuplicates}
disabled={state.duplicateEntryIndex !== undefined && state.duplicateEntryIndex !== index}
classNames={{ input: state.cssClass.input }}
/>
<Show when={state.duplicateEntryIndex === index}>
<span class={listStyles.error}>
<ExclamationTriangle svgAttrs={{ class: listStyles['svg'], 'aria-hidden': true }} />
Duplicate entries not allowed.
</span>
</Show>
</div>
)}
</For>
<div>
<Button
type='button'
variant='outline'
classNames={listStyles['add']}
onClick={(event) => state.addAnother()}
name='Add URL'
disabled={state.duplicateEntryIndex !== undefined}
/>
</div>
</div>
</fieldset>
);
}
42 changes: 42 additions & 0 deletions src/shared/inputs/ItemList/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.rowContainer {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 0.5rem;
}

.error {
margin-top: 0.25rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
line-height: 0.5rem;
color: #ef4444;
}

.row {
display: flex;
align-items: center;
gap: 0.75rem;
}

.input {
flex: 1;
font-size: 0.875rem !important;
}

.add {
height: 2rem;
}

.svg {
height: 1.25rem;
width: 1.25rem;
color: #ef4444;
}

:disabled .svg {
color: grey;
cursor: not-allowed;
}
8 changes: 8 additions & 0 deletions src/shared/inputs/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@
display: flex;
justify-content: space-between;
}

.fieldset {
border: 0;
}

.input-sm {
height: 2rem !important;
}
32 changes: 16 additions & 16 deletions src/sso/connections/CreateConnection/oidc/index.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import Spacer from '../../../../shared/Spacer/index.lite';
import Separator from '../../../../shared/Separator/index.lite';
import Anchor from '../../../../shared/Anchor/index.lite';
import InputField from '../../../../shared/inputs/InputField/index.lite';
import TextArea from '../../../../shared/inputs/TextArea/index.lite';
import SecretInputFormControl from '../../../../shared/inputs/SecretInputFormControl/index.lite';
import Select from '../../../../shared/Select/index.lite';
import ItemList from '../../../../shared/inputs/ItemList/index.lite';

const DEFAULT_VALUES = {
variant: 'basic',
Expand All @@ -23,7 +23,7 @@ const INITIAL_VALUES = {
description: '',
tenant: '',
product: '',
redirectUrl: '',
redirectUrl: [''],
defaultRedirectUrl: '',
oidcClientSecret: '',
oidcClientId: '',
Expand Down Expand Up @@ -53,10 +53,13 @@ export default function CreateOIDCConnection(props: CreateConnectionProps) {
const targetValue = (event.currentTarget as HTMLInputElement | HTMLTextAreaElement)?.value as Values;
state.oidcConnection = state.updateConnection({ [id]: targetValue });
},
handleItemListUpdate(fieldName: string, listValue: string[]) {
state.oidcConnection = state.updateConnection({ [fieldName]: listValue });
},
save(event: Event) {
event.preventDefault();

const formObj = {} as Partial<OIDCSSOConnection>;
const formObj = {} as any;
Object.entries(state.oidcConnection).map(([key, val]) => {
if (key.startsWith('oidcMetadata.')) {
if (formObj.oidcMetadata === undefined) {
Expand All @@ -67,7 +70,7 @@ export default function CreateOIDCConnection(props: CreateConnectionProps) {
// pass sortOrder only if set to non-empty string
val !== '' && (formObj[key] = +val); // convert sortOrder into number
} else {
formObj[key as keyof Omit<OIDCSSOConnection, 'oidcMetadata'>] = val as string;
formObj[key] = val;
}
});
state.isSaving = true;
Expand Down Expand Up @@ -253,20 +256,17 @@ export default function CreateOIDCConnection(props: CreateConnectionProps) {
<Spacer y={6} />
</Show>
<Show when={!state.isExcluded('redirectUrl')}>
<TextArea
label='Allowed redirect URLs (newline separated)'
id='redirectUrl'
classNames={state.classes.textarea}
required
readOnly={state.isReadOnly('redirectUrl')}
aria-describedby='redirectUrl-hint'
placeholder='http://localhost:3366'
value={state.oidcConnection.redirectUrl}
handleInputChange={state.handleChange}
<ItemList
inputType='url'
label='Allowed redirect URLs'
currentlist={state.oidcConnection.redirectUrl}
fieldName='redirectUrl'
handleItemListUpdate={state.handleItemListUpdate}
classNames={state.classes.inputField}
/>
<div id='redirectUrl-hint' class={defaultClasses.hint}>
URL to redirect the user to after login. You can specify multiple URLs by separating them with a
new line.
URL(s) to redirect the user to after login. Only the URLs in this list are allowed in the OAuth
flow.
</div>
<Spacer y={6} />
</Show>
Expand Down
27 changes: 14 additions & 13 deletions src/sso/connections/CreateConnection/saml/index.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Checkbox from '../../../../shared/Checkbox/index.lite';
import InputField from '../../../../shared/inputs/InputField/index.lite';
import TextArea from '../../../../shared/inputs/TextArea/index.lite';
import Select from '../../../../shared/Select/index.lite';
import ItemList from '../../../../shared/inputs/ItemList/index.lite';

const DEFAULT_VALUES = {
variant: 'basic',
Expand All @@ -23,7 +24,7 @@ const INITIAL_VALUES = {
description: '',
tenant: '',
product: '',
redirectUrl: '',
redirectUrl: [''],
defaultRedirectUrl: '',
rawMetadata: '',
metadataUrl: '',
Expand Down Expand Up @@ -51,6 +52,9 @@ export default function CreateSAMLConnection(props: CreateConnectionProps) {

state.samlConnection = state.updateConnection({ [id]: targetValue });
},
handleItemListUpdate(fieldName: string, listValue: string[]) {
state.samlConnection = state.updateConnection({ [fieldName]: listValue });
},
save(event: Event) {
event.preventDefault();
state.isSaving = true;
Expand Down Expand Up @@ -237,20 +241,17 @@ export default function CreateSAMLConnection(props: CreateConnectionProps) {
<Spacer y={6} />
</Show>
<Show when={!state.isExcluded('redirectUrl')}>
<TextArea
label='Allowed redirect URLs (newline separated)'
id='redirectUrl'
classNames={state.classes.textarea}
required
readOnly={state.isReadOnly('redirectUrl')}
aria-describedby='redirectUrl-hint'
placeholder='http://localhost:3366'
value={state.samlConnection.redirectUrl}
handleInputChange={state.handleChange}
<ItemList
inputType='url'
label='Allowed redirect URLs'
currentlist={state.samlConnection.redirectUrl}
fieldName='redirectUrl'
handleItemListUpdate={state.handleItemListUpdate}
classNames={state.classes.inputField}
/>
<div id='redirectUrl-hint' class={defaultClasses.hint}>
URL to redirect the user to after login. You can specify multiple URLs by separating them with a
new line.
URL(s) to redirect the user to after login. Only the URLs in this list are allowed in the OAuth
flow.
</div>
<Spacer y={6} />
</Show>
Expand Down
Loading