fix: add form validation
This commit is contained in:
@@ -290,5 +290,11 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -290,7 +290,13 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -393,5 +399,11 @@ export default {
|
|||||||
// 78. 'models.form.enableModelRoute.tips': 'Enable Model Route',
|
// 78. 'models.form.enableModelRoute.tips': 'Enable Model Route',
|
||||||
// 79. 'models.table.modelView': 'Model View',
|
// 79. 'models.table.modelView': 'Model View',
|
||||||
// 80. 'models.table.instanceView': 'Instance View',
|
// 80. 'models.table.instanceView': 'Instance View',
|
||||||
// 81. 'models.table.category': 'Category'
|
// 81. 'models.table.category': 'Category',
|
||||||
|
// 82. 'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
// 83. 'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
// 84. 'models.form.lora.select': 'Select LoRA',
|
||||||
|
// 85. 'models.form.lora.name': 'LoRA name',
|
||||||
|
// 86. 'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
// 87. 'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -294,7 +294,13 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
@@ -307,5 +313,11 @@ export default {
|
|||||||
// 5. 'models.form.backend.sglang': 'Built-in support for NVIDIA, AMD, Ascend, Moore Threads, MetaX, T-Head PPU devices.',
|
// 5. 'models.form.backend.sglang': 'Built-in support for NVIDIA, AMD, Ascend, Moore Threads, MetaX, T-Head PPU devices.',
|
||||||
// 6. 'models.table.modelView': 'Model List',
|
// 6. 'models.table.modelView': 'Model List',
|
||||||
// 7. 'models.table.instanceView': 'Instance List',
|
// 7. 'models.table.instanceView': 'Instance List',
|
||||||
// 8. 'models.table.category': 'Category'
|
// 8. 'models.table.category': 'Category',
|
||||||
|
// 9. 'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
// 10. 'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
// 11. 'models.form.lora.select': 'Select LoRA',
|
||||||
|
// 12. 'models.form.lora.name': 'LoRA name',
|
||||||
|
// 13. 'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
// 14. 'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -290,11 +290,23 @@ export default {
|
|||||||
'models.instance.previousRun': 'Previous Run',
|
'models.instance.previousRun': 'Previous Run',
|
||||||
'models.instance.startHistory': 'Run History',
|
'models.instance.startHistory': 'Run History',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'Shows logs from the run before the last error-triggered restart.'
|
'Shows logs from the run before the last error-triggered restart.',
|
||||||
|
'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
'models.form.lora.select': 'Select LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA name',
|
||||||
|
'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
// 1. 'models.table.modelView': 'Model List',
|
// 1. 'models.table.modelView': 'Model List',
|
||||||
// 2. 'models.table.instanceView': 'Instance List',
|
// 2. 'models.table.instanceView': 'Instance List',
|
||||||
// 3. 'models.table.category': 'Category'
|
// 3. 'models.table.category': 'Category',
|
||||||
|
// 4. 'models.form.lora.label': 'LoRA Adapter',
|
||||||
|
// 5. 'models.form.lora.add': 'Add LoRA Adapter',
|
||||||
|
// 6. 'models.form.lora.select': 'Select LoRA',
|
||||||
|
// 7. 'models.form.lora.name': 'LoRA name',
|
||||||
|
// 8. 'models.form.lora.rule.empty': 'Input cannot be empty',
|
||||||
|
// 9. 'models.form.lora.rule.duplicate': 'LoRA name cannot be duplicated'
|
||||||
// ========== End of To-Do List ==========
|
// ========== End of To-Do List ==========
|
||||||
|
|||||||
@@ -274,5 +274,11 @@ export default {
|
|||||||
'models.instance.previousRun': '上一次运行',
|
'models.instance.previousRun': '上一次运行',
|
||||||
'models.instance.startHistory': '运行记录',
|
'models.instance.startHistory': '运行记录',
|
||||||
'models.instance.startHistory.tips':
|
'models.instance.startHistory.tips':
|
||||||
'显示上一次因错误自动重启之前的那次运行的日志。'
|
'显示上一次因错误自动重启之前的那次运行的日志。',
|
||||||
|
'models.form.lora.label': 'LoRA 适配器',
|
||||||
|
'models.form.lora.add': '添加 LoRA 适配器',
|
||||||
|
'models.form.lora.select': '选择 LoRA',
|
||||||
|
'models.form.lora.name': 'LoRA 名称',
|
||||||
|
'models.form.lora.rule.empty': '输入不能为空',
|
||||||
|
'models.form.lora.rule.duplicate': 'LoRA name 不能重复'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ import {
|
|||||||
Input as CInput,
|
Input as CInput,
|
||||||
Cascader as SealCascader
|
Cascader as SealCascader
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import useQueryModelLoraList, {
|
import useQueryModelLoraList, {
|
||||||
LoraOptionGroup
|
LoraOptionGroup
|
||||||
} from '../services/use-query-lora-list';
|
} from '../services/use-query-lora-list';
|
||||||
|
import loraSelectionStyles from '../style/lora-selection.less';
|
||||||
|
|
||||||
interface LoraListItemProps {
|
interface LoraListItemProps {
|
||||||
item: { value: any[]; lora_name: string };
|
item: { value: any[]; lora_name: string };
|
||||||
base: string;
|
base: string;
|
||||||
defaultDataList: LoraOptionGroup[];
|
defaultDataList: LoraOptionGroup[];
|
||||||
selectedRepoNames: Set<string>;
|
selectedRepoNames: Set<string>;
|
||||||
|
duplicateNames: Set<string>;
|
||||||
|
validated: boolean;
|
||||||
onChange: (partial: { value?: any[]; lora_name?: string }) => void;
|
onChange: (partial: { value?: any[]; lora_name?: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,8 +26,11 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
|
|||||||
base,
|
base,
|
||||||
defaultDataList,
|
defaultDataList,
|
||||||
selectedRepoNames,
|
selectedRepoNames,
|
||||||
|
duplicateNames,
|
||||||
|
validated,
|
||||||
onChange
|
onChange
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const { dataList: ownSearchList, fetchData } = useQueryModelLoraList();
|
const { dataList: ownSearchList, fetchData } = useQueryModelLoraList();
|
||||||
const [hasSearched, setHasSearched] = useState(false);
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
|
|
||||||
@@ -79,11 +86,15 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cascaderEmpty = !item.value || item.value.length === 0;
|
const cascaderEmpty = !item.value || item.value.length === 0;
|
||||||
const nameEmpty = !item.lora_name;
|
const nameEmpty = !item.lora_name?.trim();
|
||||||
|
const isDuplicate =
|
||||||
|
!!item.lora_name?.trim() && duplicateNames.has(item.lora_name.trim());
|
||||||
|
|
||||||
const cascaderStatus =
|
const cascaderStatus =
|
||||||
cascaderEmpty && !nameEmpty ? ('error' as const) : undefined;
|
validated && cascaderEmpty ? ('error' as const) : 'success';
|
||||||
|
|
||||||
const inputStatus =
|
const inputStatus =
|
||||||
nameEmpty && !cascaderEmpty ? ('error' as const) : undefined;
|
validated && (nameEmpty || isDuplicate) ? ('error' as const) : 'success';
|
||||||
|
|
||||||
const displayRender = (labels: any[]) => {
|
const displayRender = (labels: any[]) => {
|
||||||
return (
|
return (
|
||||||
@@ -105,41 +116,27 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
|
|||||||
|
|
||||||
const optionNode = (option: any) => {
|
const optionNode = (option: any) => {
|
||||||
const { data } = option;
|
const { data } = option;
|
||||||
if (data.isParent) {
|
|
||||||
return (
|
|
||||||
<AutoTooltip ghost maxWidth={100}>
|
|
||||||
{data.label}
|
|
||||||
</AutoTooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<AutoTooltip ghost maxWidth={180}>
|
<AutoTooltip ghost maxWidth={'100%'}>
|
||||||
{data.label}
|
{data.label}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={loraSelectionStyles.item}>
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: '1fr 120px',
|
|
||||||
gap: 8,
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SealCascader
|
<SealCascader
|
||||||
showSearch
|
expandTrigger="click"
|
||||||
expandTrigger="hover"
|
|
||||||
multiple={false}
|
multiple={false}
|
||||||
alwaysFocus={true}
|
alwaysFocus={true}
|
||||||
status={cascaderStatus}
|
status={cascaderStatus}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
options={groupedOptions}
|
options={groupedOptions}
|
||||||
onChange={handleCascaderChange}
|
onChange={handleCascaderChange}
|
||||||
onSearch={handleSearch}
|
showSearch={{
|
||||||
placeholder="Select LoRA"
|
onSearch: handleSearch
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({ id: 'models.form.lora.select' })}
|
||||||
showCheckedStrategy="SHOW_CHILD"
|
showCheckedStrategy="SHOW_CHILD"
|
||||||
displayRender={displayRender}
|
displayRender={displayRender}
|
||||||
optionNode={optionNode}
|
optionNode={optionNode}
|
||||||
@@ -158,11 +155,10 @@ const LoraListItem: React.FC<LoraListItemProps> = ({
|
|||||||
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
||||||
></SealCascader>
|
></SealCascader>
|
||||||
<CInput.Input
|
<CInput.Input
|
||||||
style={{ flex: 1 }}
|
|
||||||
status={inputStatus}
|
status={inputStatus}
|
||||||
value={item.lora_name}
|
value={item.lora_name}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
placeholder="LoRA name"
|
placeholder={intl.formatMessage({ id: 'models.form.lora.name' })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MetadataList } from '@gpustack/core-ui';
|
import { MetadataList } from '@gpustack/core-ui';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FormData, LoraListItem } from '../config/types';
|
import { FormData, LoraListItem } from '../config/types';
|
||||||
@@ -8,6 +9,7 @@ import LoraItem from './lora-list-item';
|
|||||||
type ItemValue = { value: any[]; lora_name: string };
|
type ItemValue = { value: any[]; lora_name: string };
|
||||||
|
|
||||||
const ModelLoraList = () => {
|
const ModelLoraList = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const form = Form.useFormInstance<FormData>();
|
const form = Form.useFormInstance<FormData>();
|
||||||
const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form);
|
const huggingfaceRepoId = Form.useWatch('huggingface_repo_id', form);
|
||||||
const modelScopeModelId = Form.useWatch('model_scope_model_id', form);
|
const modelScopeModelId = Form.useWatch('model_scope_model_id', form);
|
||||||
@@ -18,6 +20,7 @@ const ModelLoraList = () => {
|
|||||||
const { dataList: defaultDataList, fetchData } = useQueryModelLoraList();
|
const { dataList: defaultDataList, fetchData } = useQueryModelLoraList();
|
||||||
|
|
||||||
const [itemList, setItemList] = useState<ItemValue[]>([]);
|
const [itemList, setItemList] = useState<ItemValue[]>([]);
|
||||||
|
const [validated, setValidated] = useState(false);
|
||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
const prevBaseRef = useRef<string>('');
|
const prevBaseRef = useRef<string>('');
|
||||||
|
|
||||||
@@ -59,6 +62,19 @@ const ModelLoraList = () => {
|
|||||||
);
|
);
|
||||||
}, [itemList]);
|
}, [itemList]);
|
||||||
|
|
||||||
|
const duplicateNames = useMemo(() => {
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
itemList.forEach((it) => {
|
||||||
|
const n = it.lora_name?.trim();
|
||||||
|
if (n) counts.set(n, (counts.get(n) ?? 0) + 1);
|
||||||
|
});
|
||||||
|
return new Set(
|
||||||
|
Array.from(counts.entries())
|
||||||
|
.filter(([, c]) => c > 1)
|
||||||
|
.map(([n]) => n)
|
||||||
|
);
|
||||||
|
}, [itemList]);
|
||||||
|
|
||||||
const syncFormField = (newItemList: ItemValue[]) => {
|
const syncFormField = (newItemList: ItemValue[]) => {
|
||||||
const newFormList = newItemList
|
const newFormList = newItemList
|
||||||
.map((it) => ({
|
.map((it) => ({
|
||||||
@@ -66,10 +82,16 @@ const ModelLoraList = () => {
|
|||||||
lora_repo_name: it.value?.[1] || '',
|
lora_repo_name: it.value?.[1] || '',
|
||||||
lora_name: it.lora_name || ''
|
lora_name: it.lora_name || ''
|
||||||
}))
|
}))
|
||||||
.filter((it) => it.lora_repo_name?.trim() && it.lora_name?.trim());
|
.filter((it) => it.lora_repo_name?.trim() || it.lora_name?.trim());
|
||||||
form.setFieldValue('lora_list', newFormList);
|
form.setFieldValue('lora_list', newFormList);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (validated) {
|
||||||
|
form.validateFields(['lora_list']).catch(() => {});
|
||||||
|
}
|
||||||
|
}, [itemList, validated]);
|
||||||
|
|
||||||
const handleItemChange = (
|
const handleItemChange = (
|
||||||
index: number,
|
index: number,
|
||||||
partial: { value?: any[]; lora_name?: string }
|
partial: { value?: any[]; lora_name?: string }
|
||||||
@@ -93,11 +115,42 @@ const ModelLoraList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item<FormData> name="lora_list">
|
<Form.Item<FormData>
|
||||||
|
name="lora_list"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: async (_, value: LoraListItem[]) => {
|
||||||
|
if (!validated) {
|
||||||
|
setValidated(true);
|
||||||
|
}
|
||||||
|
if (!value || value.length === 0) return;
|
||||||
|
|
||||||
|
for (const it of value) {
|
||||||
|
const hasRepo = !!it.lora_repo_name?.trim();
|
||||||
|
const hasName = !!it.lora_name?.trim();
|
||||||
|
if (hasRepo !== hasName) {
|
||||||
|
throw new Error(
|
||||||
|
intl.formatMessage({ id: 'models.form.lora.rule.empty' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = value
|
||||||
|
.map((it) => it.lora_name?.trim())
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
if (names.length !== new Set(names).size) {
|
||||||
|
throw new Error(
|
||||||
|
intl.formatMessage({ id: 'models.form.lora.rule.duplicate' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<MetadataList
|
<MetadataList
|
||||||
label="LoRA Adapter"
|
label={intl.formatMessage({ id: 'models.form.lora.label' })}
|
||||||
dataList={itemList}
|
dataList={itemList}
|
||||||
btnText="Add LoRA Adapter"
|
btnText={intl.formatMessage({ id: 'models.form.lora.add' })}
|
||||||
onAdd={handleAdd}
|
onAdd={handleAdd}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
>
|
>
|
||||||
@@ -107,6 +160,8 @@ const ModelLoraList = () => {
|
|||||||
base={base}
|
base={base}
|
||||||
defaultDataList={defaultDataList}
|
defaultDataList={defaultDataList}
|
||||||
selectedRepoNames={selectedRepoNames}
|
selectedRepoNames={selectedRepoNames}
|
||||||
|
duplicateNames={duplicateNames}
|
||||||
|
validated={validated}
|
||||||
onChange={(partial) => handleItemChange(index, partial)}
|
onChange={(partial) => handleItemChange(index, partial)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ export const useQueryModelLoraList = () => {
|
|||||||
const [dataList, setDataList] = useState<LoraOptionGroup[]>([]);
|
const [dataList, setDataList] = useState<LoraOptionGroup[]>([]);
|
||||||
|
|
||||||
const getData = (params: Parameters) => {
|
const getData = (params: Parameters) => {
|
||||||
fetchData(params).then((result) => {
|
fetchData({
|
||||||
|
...params,
|
||||||
|
limit: params.limit || 40
|
||||||
|
}).then((result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
const groups: Record<string, LoraOptionGroup> = {};
|
const groups: Record<string, LoraOptionGroup> = {};
|
||||||
result.lora_list.forEach((item) => {
|
result.lora_list.forEach((item) => {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
:global {
|
||||||
|
.ant-select-dropdown {
|
||||||
|
> div {
|
||||||
|
.ant-cascader-menus {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:nth-child(1) {
|
||||||
|
flex: 1 1 320px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
> *:nth-child(2) {
|
||||||
|
flex: 1 1 120px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user