Skip to content

Commit

Permalink
feat: shared improvement (kubesphere#751)
Browse files Browse the repository at this point in the history
* feat: add interval hooks

Signed-off-by: elichen95 <[email protected]>

* fix: fix use actions bugs

Signed-off-by: elichen95 <[email protected]>

* feat: add Object input and properties input component

Signed-off-by: elichen95 <[email protected]>

* feat: add TimeSelector component

Signed-off-by: elichen95 <[email protected]>

* fix: fix detail left side styles

Signed-off-by: elichen95 <[email protected]>

* feat: export Inputs and TimeSelector components

Signed-off-by: elichen95 <[email protected]>

---------

Signed-off-by: elichen95 <[email protected]>
  • Loading branch information
EliChen95 authored Feb 2, 2023
1 parent 0a31edb commit d2eca12
Show file tree
Hide file tree
Showing 17 changed files with 1,020 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const BaseWrapper = styled.div`

const BaseTitle = styled.div`
margin-bottom: 4px;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
Expand All @@ -27,6 +26,12 @@ const BaseTitle = styled.div`
font-weight: 600;
color: #36435c;
display: flex;
span {
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
overflow: hidden;
}
.kubed-icon {
margin-right: 8px;
Expand Down
72 changes: 72 additions & 0 deletions packages/shared/src/components/Inputs/ObjectInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, {
Children,
cloneElement,
isValidElement,
PropsWithChildren,
useEffect,
useState,
} from 'react';
import classNames from 'classnames';
import { get, isUndefined } from 'lodash';
import { Wrapper } from './styles';

interface Props {
name?: string;
value?: Record<string, any>;
defaultValue?: Record<string, any>;
onChange?: (value: Record<string, any>) => void;
}

function ObjectInput({
value = {},
defaultValue = {},
onChange,
children,
}: PropsWithChildren<Props>) {
const [selfValue, setSelfValue] = useState<Record<string, any>>(defaultValue ?? {});

useEffect(() => {
setSelfValue(value);
}, [value]);

const getValue = (name: string, childDefaultValue: any) => {
if (!isUndefined(selfValue[name])) {
return selfValue[name];
}

if (!isUndefined(childDefaultValue)) {
setSelfValue({
...selfValue,
[name]: childDefaultValue,
});
}

return childDefaultValue;
};

const childNodes = Children.map(children, child =>
isValidElement(child)
? cloneElement(child, {
...child.props,
className: classNames(child.props.className, 'item'),
value: isUndefined(selfValue)
? child.props.value
: getValue(child.props.name, child.props.defaultValue),
onChange: (val: any) => {
const childValue = get(val, 'currentTarget.value', val);
const newValue = {
...selfValue,
[child.props.name]: childValue,
};
setSelfValue(newValue);
onChange?.(newValue);
child.props?.onChange?.(childValue);
},
})
: child,
);

return <Wrapper>{childNodes}</Wrapper>;
}

export default ObjectInput;
11 changes: 11 additions & 0 deletions packages/shared/src/components/Inputs/ObjectInput/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import styled from 'styled-components';

export const Wrapper = styled.div`
display: flex;
.item {
& + .item {
margin-left: 12px;
}
}
`;
168 changes: 168 additions & 0 deletions packages/shared/src/components/Inputs/PropertiesInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { has, isEmpty } from 'lodash';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import PropertyItem, { Props as ItemProps } from './item';
import { AddButton, Wrapper } from './styles';

interface Props {
name?: string;
value?: Record<string, any>;
hiddenKeys?: string[];
readOnlyKeys?: string[];
onChange?: (value: Record<string, any>) => void;
onError?: (error?: { message?: string }) => void;
itemProps?: Partial<ItemProps>;
addText?: ReactNode;
}

interface ValueType {
key: string;
value?: any;
}

function PropertiesInput({
value,
hiddenKeys = [],
readOnlyKeys = [],
onChange,
onError,
itemProps,
addText = t('ADD'),
}: Props) {
const [selfValue, setSelfValue] = useState<Record<string, any>>(value ?? {});
const [hiddenValues, setHiddenValues] = useState<ValueType[]>([]);
const [readOnlyValues, setReadOnlyValues] = useState<ValueType[]>([]);
const [arrayValues, setArrayValues] = useState<ValueType[]>([]);

useEffect(() => {
if (value) {
setSelfValue(value);
}
}, [value]);

useEffect(() => {
const newHiddenValues: ValueType[] = [];
const newReadOnlyValues: ValueType[] = [];
const newArrayValues: ValueType[] = [];

Object.keys(selfValue).forEach(key => {
if (hiddenKeys.some(hiddenKey => new RegExp(hiddenKey).test(key))) {
newHiddenValues.push({
key,
value: selfValue[key],
});
} else if (readOnlyKeys.some(readOnlyKey => new RegExp(readOnlyKey).test(key))) {
newReadOnlyValues.push({
key,
value: selfValue[key],
});
} else {
newArrayValues.push({
key,
value: selfValue[key],
});
}
});

if (isEmpty(newArrayValues) && isEmpty(newReadOnlyValues)) {
newArrayValues.push({ key: '' });
}

setArrayValues(newArrayValues);
setHiddenValues(newHiddenValues);
setReadOnlyValues(newReadOnlyValues);
}, [selfValue]);

const handleAdd = () => {
setArrayValues([...arrayValues, { key: '' }]);
};

const triggerChange = (values: ValueType[]) => {
let existedKey = false;
let emptyKeyValue = false;

onError?.();
const valuePairs = [...hiddenValues, ...readOnlyValues, ...values];
const newValue = valuePairs.reduce((prev, cur) => {
cur.key = cur.key || '';

// when add new line, do not change value
if (isEmpty(cur.key) && isEmpty(cur.value)) {
emptyKeyValue = true;
}

// has duplicate keys
if (has(prev, cur.key)) {
existedKey = true;
return prev;
}

return {
...prev,
[cur.key]: cur.value || '',
};
}, {});

setArrayValues(values);

const hasEmptyKey = Object.keys(newValue).some(key => isEmpty(key));
if (hasEmptyKey) {
onError?.({ message: t('EMPTY_KEY') });
} else if (existedKey) {
onError?.({ message: t('DUPLICATE_KEYS') });
} else {
onError?.();
}

if (emptyKeyValue) {
return;
}
if (!existedKey) {
onChange?.(newValue);
}
};

const handleItemChange = (index: number, val: ValueType) => {
arrayValues[index] = val;
triggerChange([...arrayValues]);
};

const handleItemDelete = (index: number) => {
triggerChange([...arrayValues.filter((_, _index) => _index !== index)]);
};

const isAddEnable = useMemo(
() => arrayValues.every(item => !(isEmpty(item) || (!item.key && !item.value))),
[arrayValues],
);

return (
<Wrapper>
{readOnlyValues.map((item, index) => (
<PropertyItem
index={index}
key={`readonly-${item.key}`}
value={item}
readOnly
{...itemProps}
></PropertyItem>
))}
{arrayValues.map((item, index) => (
<PropertyItem
key={`array-${index}`}
index={index}
value={item || {}}
onChange={handleItemChange}
onDelete={handleItemDelete}
{...itemProps}
/>
))}
<div style={{ textAlign: 'right' }}>
<AddButton onClick={handleAdd} disabled={!isAddEnable}>
{addText}
</AddButton>
</div>
</Wrapper>
);
}

export default PropertiesInput;
53 changes: 53 additions & 0 deletions packages/shared/src/components/Inputs/PropertiesInput/item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { ComponentType } from 'react';
import { Input } from '@kubed/components';
import { Trash } from '@kubed/icons';
import ObjectInput from '../ObjectInput';
import { DeleteButton, Item } from './styles';

interface ValueType {
key: string;
value?: any;
}

export interface Props {
index: number;
value?: ValueType;
readOnly?: boolean;
keyProps?: { component?: ComponentType; [key: string]: any };
valueProps?: { component?: ComponentType; [key: string]: any };
onChange?: (index: number, value: any) => void;
onDelete?: (index: number) => void;
}

function PropertyItem({
index,
readOnly,
keyProps = {},
valueProps = {},
onChange,
onDelete,
...rest
}: Props) {
const { component: KeyInput = Input, ...keyInputProps } = keyProps;
const { component: ValueInput = Input, ...valueInputProps } = valueProps;
return (
<Item>
<ObjectInput {...rest} onChange={value => onChange?.(index, value)}>
<KeyInput name="key" placeholder={t('KEY')} readOnly={readOnly} {...keyInputProps} />
<ValueInput
name="value"
placeholder={t('VALUE')}
readOnly={readOnly}
{...valueInputProps}
/>
</ObjectInput>
{!readOnly && (
<DeleteButton onClick={() => onDelete?.(index)}>
<Trash />
</DeleteButton>
)}
</Item>
);
}

export default PropertyItem;
40 changes: 40 additions & 0 deletions packages/shared/src/components/Inputs/PropertiesInput/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Button } from '@kubed/components';
import styled from 'styled-components';

export const Wrapper = styled.div`
position: relative;
`;

export const Item = styled.div`
position: relative;
padding: 6px 68px 6px 17px;
border-radius: 60px;
background-color: ${({ theme }) => theme.palette.background};
border: solid 1px ${({ theme }) => theme.palette.border};
& + .item {
margin-top: 8px;
}
& > input {
max-width: none !important;
}
`;

export const DeleteButton = styled(Button)`
right: 6px;
position: absolute;
top: 50%;
transform: translateY(-50%);
`;

export const AddButton = styled(Button)`
margin-top: 12px;
`;

export const Desc = styled.div`
position: absolute;
left: 0;
bottom: 14px;
color: ${({ theme }) => theme.palette.accents_5};
`;
2 changes: 2 additions & 0 deletions packages/shared/src/components/Inputs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as ObjectInput } from './ObjectInput';
export { default as PropertiesInput } from './PropertiesInput';
Loading

0 comments on commit d2eca12

Please sign in to comment.