chore: Edit Row changes (#2564)

* chore: checklist value and popup

* chore: properties side panel

* chore: reorganize checklist field

* chore: delete property promt

* chore: delete property reorganize

* fix: dnd bug of checklist field

* fix: whitespace on empty fields

* chore: new checklist item

* fix: duplicate view

* fix: named checklist bars

* chore: checklist padding

* fix: onclick

* chore: change to nullish coalescing operator

* chore: remove empty string from use translation

* fix: add missing translations

* chore: refactor select option and checklist field editors
This commit is contained in:
Askarbek Zadauly 2023-05-27 19:12:14 +06:00 committed by GitHub
parent 9a213fa562
commit 6935653e15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1281 additions and 432 deletions

View File

@ -5,7 +5,7 @@ export const Button = ({
children,
onClick,
}: {
size?: 'primary' | 'medium' | 'small' | 'box-small-transparent';
size?: 'primary' | 'medium' | 'small' | 'box-small-transparent' | 'medium-transparent';
children: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement>;
}) => {
@ -21,6 +21,11 @@ export const Button = ({
case 'small':
setCls('w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-main-accent text-white text-xs');
break;
case 'medium-transparent':
setCls(
'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-main-accent text-main-accent transition-colors duration-300 hover:bg-main-hovered hover:text-white'
);
break;
case 'box-small-transparent':
setCls('text-black hover:text-main-accent w-[24px] h-[24px]');
break;

View File

@ -0,0 +1,27 @@
export const CheckListProgress = ({ completed, max }: { completed: number; max: number }) => {
return (
<div className={'flex w-full items-center gap-4 py-1'}>
{max > 0 && (
<>
<div className={'flex flex-1 gap-1'}>
{completed > 0 && filledCheckListBars({ amount: completed })}
{max - completed > 0 && emptyCheckListBars({ amount: max - completed })}
</div>
<div className={'text-xs text-shade-4'}>{((100 * completed) / max).toFixed(0)}%</div>
</>
)}
</div>
);
};
const filledCheckListBars = ({ amount }: { amount: number }) => {
return Array(amount)
.fill(0)
.map((item, index) => <div key={index} className={'h-[4px] flex-1 flex-shrink-0 rounded bg-main-accent'}></div>);
};
const emptyCheckListBars = ({ amount }: { amount: number }) => {
return Array(amount)
.fill(0)
.map((item, index) => <div key={index} className={'h-[4px] flex-1 flex-shrink-0 rounded bg-tint-9'}></div>);
};

View File

@ -1,156 +0,0 @@
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { SelectOptionCellDataPB, SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
import { getBgColor } from '$app/components/_shared/getColor';
import { useTranslation } from 'react-i18next';
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { useAppSelector } from '$app/stores/store';
import { ISelectOption, ISelectOptionType } from '$app/stores/reducers/database/slice';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
export const CellOptionsPopup = ({
top,
left,
cellIdentifier,
cellCache,
fieldController,
onOutsideClick,
openOptionDetail,
}: {
top: number;
left: number;
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
onOutsideClick: () => void;
openOptionDetail: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation('');
const [value, setValue] = useState('');
const { data } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database);
useEffect(() => {
if (inputRef?.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onKeyDown: KeyboardEventHandler = async (e) => {
if (e.key === 'Enter' && value.length > 0) {
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
setValue('');
}
};
const onUnselectOptionClick = async (option: SelectOptionPB) => {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
setValue('');
};
const onToggleOptionClick = async (option: SelectOptionPB) => {
if ((data as SelectOptionCellDataPB)?.select_options?.find((selectedOption) => selectedOption.id === option.id)) {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
} else {
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
}
setValue('');
};
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
if (e.key === 'Escape') {
onOutsideClick();
}
};
const onOptionDetailClick = (e: any, option: ISelectOption) => {
e.stopPropagation();
let target = e.target as HTMLElement;
while (!(target instanceof HTMLButtonElement)) {
if (target.parentElement === null) return;
target = target.parentElement;
}
const selectOption = new SelectOptionPB({
id: option.selectOptionId,
name: option.title,
color: option.color || SelectOptionColorPB.Purple,
});
const { right: _left, top: _top } = target.getBoundingClientRect();
openOptionDetail(_left, _top, selectOption);
};
return (
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
<div className={'flex flex-wrap items-center gap-2 text-black'}>
{(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`} key={index}>
<span>{option?.name || ''}</span>
<button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}>
<CloseSvg></CloseSvg>{' '}
</button>
</div>
)) || ''}
</div>
<input
ref={inputRef}
className={'py-2'}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={t('grid.selectOption.searchOption') || ''}
onKeyDown={onKeyDown}
/>
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
</div>
<div className={'-mx-4 h-[1px] bg-shade-6'}></div>
<div className={'font-medium text-shade-3'}>{t('grid.selectOption.panelTitle') || ''}</div>
<div className={'flex flex-col gap-1'}>
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
(option, index) => (
<div
key={index}
onClick={() =>
onToggleOptionClick(
new SelectOptionPB({
id: option.selectOptionId,
name: option.title,
color: option.color || SelectOptionColorPB.Purple,
})
)
}
className={
'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'
}
>
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div>
<div className={'flex items-center'}>
{(data as SelectOptionCellDataPB)?.select_options?.find(
(selectedOption) => selectedOption.id === option.selectOptionId
) && (
<button className={'h-5 w-5 p-1'}>
<CheckmarkSvg></CheckmarkSvg>
</button>
)}
<button onClick={(e) => onOptionDetailClick(e, option)} className={'h-6 w-6 p-1'}>
<Details2Svg></Details2Svg>
</button>
</div>
</div>
)
)}
</div>
</div>
</PopupWindow>
);
};

View File

@ -0,0 +1,40 @@
import { SelectOptionCellDataPB } from '@/services/backend';
import { useEffect, useRef, useState } from 'react';
import { ISelectOptionType } from '$app_reducers/database/slice';
import { useAppSelector } from '$app/stores/store';
import { CheckListProgress } from '$app/components/_shared/CheckListProgress';
export const CheckList = ({
data,
fieldId,
onEditClick,
}: {
data: SelectOptionCellDataPB | undefined;
fieldId: string;
onEditClick: (left: number, top: number) => void;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [allOptionsCount, setAllOptionsCount] = useState(0);
const [selectedOptionsCount, setSelectedOptionsCount] = useState(0);
const databaseStore = useAppSelector((state) => state.database);
useEffect(() => {
setAllOptionsCount((databaseStore.fields[fieldId]?.fieldOptions as ISelectOptionType)?.selectOptions?.length ?? 0);
}, [databaseStore, fieldId]);
useEffect(() => {
setSelectedOptionsCount((data as SelectOptionCellDataPB)?.select_options?.length ?? 0);
}, [data]);
const onClick = () => {
if (!ref.current) return;
const { left, top } = ref.current.getBoundingClientRect();
onEditClick(left, top);
};
return (
<div ref={ref} onClick={onClick} className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs text-black'}>
<CheckListProgress completed={selectedOptionsCount} max={allOptionsCount} />
</div>
);
};

View File

@ -0,0 +1,60 @@
import { SelectOptionPB } from '@/services/backend';
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
import { ISelectOption } from '$app_reducers/database/slice';
import { MouseEventHandler } from 'react';
export const CheckListOption = ({
option,
checked,
onToggleOptionClick,
openCheckListDetail,
}: {
option: ISelectOption;
checked: boolean;
onToggleOptionClick: (v: SelectOptionPB) => void;
openCheckListDetail: (left: number, top: number, option: SelectOptionPB) => void;
}) => {
const onCheckListDetailClick: MouseEventHandler = (e) => {
e.stopPropagation();
let target = e.target as HTMLElement;
while (!(target instanceof HTMLButtonElement)) {
if (target.parentElement === null) return;
target = target.parentElement;
}
const selectOption = new SelectOptionPB({
id: option.selectOptionId,
name: option.title,
});
const { right: _left, top: _top } = target.getBoundingClientRect();
openCheckListDetail(_left, _top, selectOption);
};
return (
<div
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'}
onClick={() =>
onToggleOptionClick(
new SelectOptionPB({
id: option.selectOptionId,
name: option.title,
})
)
}
>
<div className={'h-5 w-5'}>
{checked ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
</div>
<div className={`flex-1 px-2 py-0.5`}>{option.title}</div>
<div className={'flex items-center'}>
<button onClick={onCheckListDetailClick} className={'h-6 w-6 p-1'}>
<Details2Svg></Details2Svg>
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,97 @@
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { SelectOptionCellDataPB, SelectOptionPB } from '@/services/backend';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { ISelectOptionType } from '$app_reducers/database/slice';
import { useAppSelector } from '$app/stores/store';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { useEffect, useState } from 'react';
import { CheckListProgress } from '$app/components/_shared/CheckListProgress';
import { NewCheckListOption } from '$app/components/_shared/EditRow/CheckList/NewCheckListOption';
import { CheckListOption } from '$app/components/_shared/EditRow/CheckList/CheckListOption';
import { NewCheckListButton } from '$app/components/_shared/EditRow/CheckList/NewCheckListButton';
export const CheckListPopup = ({
left,
top,
cellIdentifier,
cellCache,
fieldController,
openCheckListDetail,
onOutsideClick,
}: {
left: number;
top: number;
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
openCheckListDetail: (left: number, top: number, option: SelectOptionPB) => void;
onOutsideClick: () => void;
}) => {
const databaseStore = useAppSelector((state) => state.database);
const { data } = useCell(cellIdentifier, cellCache, fieldController);
const [allOptionsCount, setAllOptionsCount] = useState(0);
const [selectedOptionsCount, setSelectedOptionsCount] = useState(0);
const [newOptions, setNewOptions] = useState<string[]>([]);
useEffect(() => {
setAllOptionsCount(
(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType)?.selectOptions?.length ?? 0
);
}, [databaseStore, cellIdentifier]);
useEffect(() => {
setSelectedOptionsCount((data as SelectOptionCellDataPB)?.select_options?.length ?? 0);
}, [data]);
const onToggleOptionClick = async (option: SelectOptionPB) => {
if ((data as SelectOptionCellDataPB)?.select_options?.find((selectedOption) => selectedOption.id === option.id)) {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
} else {
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
}
};
return (
<PopupWindow className={'text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
<div className={'min-w-[320px]'}>
<div className={'px-4 pt-8 pb-4'}>
<CheckListProgress completed={selectedOptionsCount} max={allOptionsCount} />
</div>
<div className={'flex flex-col p-2'}>
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
(option, index) => (
<CheckListOption
key={index}
option={option}
checked={
!!(data as SelectOptionCellDataPB)?.select_options?.find((so) => so.id === option.selectOptionId)
}
onToggleOptionClick={onToggleOptionClick}
openCheckListDetail={openCheckListDetail}
></CheckListOption>
)
)}
{newOptions.map((option, index) => (
<NewCheckListOption
key={index}
index={index}
option={option}
newOptions={newOptions}
setNewOptions={setNewOptions}
cellIdentifier={cellIdentifier}
></NewCheckListOption>
))}
</div>
<div className={'h-[1px] bg-shade-6'}></div>
<div className={'p-2'}>
<NewCheckListButton newOptions={newOptions} setNewOptions={setNewOptions}></NewCheckListButton>
</div>
</div>
</PopupWindow>
);
};

View File

@ -0,0 +1,94 @@
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectOptionPB } from '@/services/backend';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
export const EditCheckListPopup = ({
left,
top,
cellIdentifier,
editingSelectOption,
onOutsideClick,
}: {
left: number;
top: number;
cellIdentifier: CellIdentifier;
editingSelectOption: SelectOptionPB;
onOutsideClick: () => void;
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const [value, setValue] = useState('');
useEffect(() => {
setValue(editingSelectOption.name);
}, [editingSelectOption]);
const onKeyDown: KeyboardEventHandler = async (e) => {
if (e.key === 'Enter' && value.length > 0) {
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
setValue('');
}
};
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
if (e.key === 'Escape') {
onOutsideClick();
}
};
const onBlur = async () => {
const svc = new SelectOptionCellBackendService(cellIdentifier);
await svc.updateOption(
new SelectOptionPB({
id: editingSelectOption.id,
name: value,
})
);
};
const onDeleteOptionClick = async () => {
const svc = new SelectOptionCellBackendService(cellIdentifier);
await svc.deleteOption([editingSelectOption]);
};
return (
<PopupWindow
className={'p-2 text-xs'}
onOutsideClick={async () => {
await onBlur();
onOutsideClick();
}}
left={left}
top={top}
>
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
<input
ref={inputRef}
className={'py-2'}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={onKeyDown}
onBlur={() => onBlur()}
/>
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
</div>
<button
onClick={() => onDeleteOptionClick()}
className={
'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-main-alert hover:bg-main-secondary'
}
>
<i className={'h-5 w-5'}>
<TrashSvg></TrashSvg>
</i>
<span>{t('grid.selectOption.deleteTag')}</span>
</button>
</div>
</PopupWindow>
);
};

View File

@ -0,0 +1,28 @@
import AddSvg from '$app/components/_shared/svg/AddSvg';
import { useTranslation } from 'react-i18next';
export const NewCheckListButton = ({
newOptions,
setNewOptions,
}: {
newOptions: string[];
setNewOptions: (v: string[]) => void;
}) => {
const { t } = useTranslation();
const newOptionClick = () => {
setNewOptions([...newOptions, '']);
};
return (
<button
onClick={() => newOptionClick()}
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-shade-6'}
>
<i className={'h-5 w-5'}>
<AddSvg></AddSvg>
</i>
<span>{t('grid.field.addOption')}</span>
</button>
);
};

View File

@ -0,0 +1,53 @@
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { useTranslation } from 'react-i18next';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
export const NewCheckListOption = ({
index,
option,
newOptions,
setNewOptions,
cellIdentifier,
}: {
index: number;
option: string;
newOptions: string[];
setNewOptions: (v: string[]) => void;
cellIdentifier: CellIdentifier;
}) => {
const { t } = useTranslation();
const updateNewOption = (value: string) => {
const newOptionsCopy = [...newOptions];
newOptionsCopy[index] = value;
setNewOptions(newOptionsCopy);
};
const onNewOptionKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
void onSaveNewOptionClick();
}
};
const onSaveNewOptionClick = async () => {
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: newOptions[index] });
setNewOptions(newOptions.filter((_, i) => i !== index));
};
return (
<div className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-shade-6'}>
<input
onKeyDown={(e) => onNewOptionKeyDown(e as unknown as KeyboardEvent)}
className={'min-w-0 flex-1 pl-7'}
value={option}
onChange={(e) => updateNewOption(e.target.value)}
/>
<button
onClick={() => onSaveNewOptionClick()}
className={'flex items-center gap-2 rounded-lg bg-main-accent px-4 py-2 text-white hover:bg-main-hovered'}
>
{t('grid.selectOption.create')}
</button>
</div>
);
};

View File

@ -4,10 +4,10 @@ import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
import { useTranslation } from 'react-i18next';
import { DateFormatPB } from '@/services/backend';
import { useDateTimeFormat } from '$app/components/_shared/EditRow/DateTimeFormat.hooks';
import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks';
import { useAppSelector } from '$app/stores/store';
import { useEffect, useState } from 'react';
import { IDateType } from '$app/stores/reducers/database/slice';
import { IDateType } from '$app_reducers/database/slice';
export const DateFormatPopup = ({
left,
@ -22,7 +22,7 @@ export const DateFormatPopup = ({
fieldController: FieldController;
onOutsideClick: () => void;
}) => {
const { t } = useTranslation('');
const { t } = useTranslation();
const { changeDateFormat } = useDateTimeFormat(cellIdentifier, fieldController);
const databaseStore = useAppSelector((state) => state.database);
const [dateType, setDateType] = useState<IDateType | undefined>();

View File

@ -8,7 +8,7 @@ import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CalendarData } from '$app/stores/effects/database/cell/controller_builder';
import { DateCellDataPB } from '@/services/backend';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { DateTypeOptions } from '$app/components/_shared/EditRow/DateTypeOptions';
import { DateTypeOptions } from '$app/components/_shared/EditRow/Date/DateTypeOptions';
export const DatePickerPopup = ({
left,

View File

@ -1,13 +1,13 @@
import { DateFormatPopup } from '$app/components/_shared/EditRow/DateFormatPopup';
import { TimeFormatPopup } from '$app/components/_shared/EditRow/TimeFormatPopup';
import { DateFormatPopup } from '$app/components/_shared/EditRow/Date/DateFormatPopup';
import { TimeFormatPopup } from '$app/components/_shared/EditRow/Date/TimeFormatPopup';
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
import { MouseEventHandler, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IDateType } from '$app/stores/reducers/database/slice';
import { IDateType } from '$app_reducers/database/slice';
import { useAppSelector } from '$app/stores/store';
import { useDateTimeFormat } from '$app/components/_shared/EditRow/DateTimeFormat.hooks';
import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
@ -18,7 +18,7 @@ export const DateTypeOptions = ({
cellIdentifier: CellIdentifier;
fieldController: FieldController;
}) => {
const { t } = useTranslation('');
const { t } = useTranslation();
const [showDateFormatPopup, setShowDateFormatPopup] = useState(false);
const [dateFormatTop, setDateFormatTop] = useState(0);
@ -105,9 +105,6 @@ export const DateTypeOptions = ({
}
>
<div className={'flex items-center gap-2'}>
{/*<i className={'h-4 w-4'}>
<ClockSvg></ClockSvg>
</i>*/}
<span>{t('grid.field.includeTime')}</span>
</div>
{/*<i className={'h-5 w-5'}>*/}

View File

@ -1,4 +1,4 @@
import { useRef } from 'react';
import { MouseEventHandler, useRef } from 'react';
import { DateCellDataPB } from '@/services/backend';
export const EditCellDate = ({
@ -10,15 +10,15 @@ export const EditCellDate = ({
}) => {
const ref = useRef<HTMLDivElement>(null);
const onClick = () => {
const onClick: MouseEventHandler = () => {
if (!ref.current) return;
const { left, top } = ref.current.getBoundingClientRect();
onEditClick(left, top);
};
return (
<div ref={ref} onClick={() => onClick()} className={'w-full px-4 py-2'}>
{data?.date || <>&nbsp;</>}
<div ref={ref} onClick={onClick} className={'w-full px-4 py-1'}>
{data?.date}&nbsp;
</div>
);
};

View File

@ -3,10 +3,7 @@ import { FieldController } from '$app/stores/effects/database/field/field_contro
import { FieldType, NumberFormatPB } from '@/services/backend';
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
import { Some } from 'ts-results';
import {
makeDateTypeOptionContext,
makeNumberTypeOptionContext,
} from '$app/stores/effects/database/field/type_option/type_option_context';
import { makeNumberTypeOptionContext } from '$app/stores/effects/database/field/type_option/type_option_context';
export const useNumberFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => {
const changeNumberFormat = async (format: NumberFormatPB) => {

View File

@ -1,12 +1,12 @@
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { useNumberFormat } from '$app/components/_shared/EditRow/NumberFormat.hooks';
import { useNumberFormat } from '$app/components/_shared/EditRow/Date/NumberFormat.hooks';
import { NumberFormatPB } from '@/services/backend';
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
import { useAppSelector } from '$app/stores/store';
import { useEffect, useState } from 'react';
import { INumberType } from '$app/stores/reducers/database/slice';
import { INumberType } from '$app_reducers/database/slice';
const list = [
{ format: NumberFormatPB.Num, title: 'Num' },

View File

@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { TimeFormatPB } from '@/services/backend';
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
import { useDateTimeFormat } from '$app/components/_shared/EditRow/DateTimeFormat.hooks';
import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks';
import { useAppSelector } from '$app/stores/store';
import { useEffect, useState } from 'react';
import { IDateType } from '$app/stores/reducers/database/slice';
import { IDateType } from '$app_reducers/database/slice';
export const TimeFormatPopup = ({
left,
@ -22,7 +22,7 @@ export const TimeFormatPopup = ({
fieldController: FieldController;
onOutsideClick: () => void;
}) => {
const { t } = useTranslation('');
const { t } = useTranslation();
const databaseStore = useAppSelector((state) => state.database);
const [dateType, setDateType] = useState<IDateType | undefined>();

View File

@ -4,15 +4,17 @@ import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
import { useAppSelector } from '$app/stores/store';
import { EditCellText } from '$app/components/_shared/EditRow/EditCellText';
import { EditCellText } from '$app/components/_shared/EditRow/InlineEditFields/EditCellText';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { EditCellDate } from '$app/components/_shared/EditRow/EditCellDate';
import { EditCellDate } from '$app/components/_shared/EditRow/Date/EditCellDate';
import { useRef } from 'react';
import { CellOptions } from '$app/components/_shared/EditRow/CellOptions';
import { EditCellNumber } from '$app/components/_shared/EditRow/EditCellNumber';
import { EditCheckboxCell } from '$app/components/_shared/EditRow/EditCheckboxCell';
import { EditCellUrl } from '$app/components/_shared/EditRow/EditCellUrl';
import { CellOptions } from '$app/components/_shared/EditRow/Options/CellOptions';
import { EditCellNumber } from '$app/components/_shared/EditRow/InlineEditFields/EditCellNumber';
import { EditCheckboxCell } from '$app/components/_shared/EditRow/InlineEditFields/EditCheckboxCell';
import { EditCellUrl } from '$app/components/_shared/EditRow/InlineEditFields/EditCellUrl';
import { Draggable } from 'react-beautiful-dnd';
import { DragElementSvg } from '$app/components/_shared/svg/DragElementSvg';
import { CheckList } from '$app/components/_shared/EditRow/CheckList/CheckList';
export const EditCellWrapper = ({
index,
@ -22,6 +24,7 @@ export const EditCellWrapper = ({
onEditFieldClick,
onEditOptionsClick,
onEditDateClick,
onEditCheckListClick,
}: {
index: number;
cellIdentifier: CellIdentifier;
@ -30,6 +33,7 @@ export const EditCellWrapper = ({
onEditFieldClick: (cell: CellIdentifier, left: number, top: number) => void;
onEditOptionsClick: (cell: CellIdentifier, left: number, top: number) => void;
onEditDateClick: (cell: CellIdentifier, left: number, top: number) => void;
onEditCheckListClick: (cell: CellIdentifier, left: number, top: number) => void;
}) => {
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database);
@ -48,26 +52,28 @@ export const EditCellWrapper = ({
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={'flex w-full items-center text-xs'}
className={'flex w-full flex-col items-start gap-2 text-xs'}
>
<div
ref={el}
onClick={() => onClick()}
className={
'relative flex w-[180px] cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 hover:bg-shade-6'
'relative flex cursor-pointer items-center gap-2 rounded-lg text-white transition-colors duration-200 hover:text-shade-3'
}
>
<div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center'}>
<div ref={el} onClick={() => onClick()} className={'flex h-5 w-5'}>
<DragElementSvg></DragElementSvg>
</div>
<div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center text-shade-3'}>
<FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
</div>
<span className={'overflow-hidden text-ellipsis whitespace-nowrap'}>
{databaseStore.fields[cellIdentifier.fieldId]?.title || ''}
<span className={'overflow-hidden text-ellipsis whitespace-nowrap text-shade-3'}>
{databaseStore.fields[cellIdentifier.fieldId]?.title ?? ''}
</span>
</div>
<div className={'flex-1 cursor-pointer rounded-lg hover:bg-shade-6'}>
<div className={'w-full cursor-pointer rounded-lg pl-3 text-sm hover:bg-shade-6'}>
{(cellIdentifier.fieldType === FieldType.SingleSelect ||
cellIdentifier.fieldType === FieldType.MultiSelect ||
cellIdentifier.fieldType === FieldType.Checklist) &&
cellIdentifier.fieldType === FieldType.MultiSelect) &&
cellController && (
<CellOptions
data={data as SelectOptionCellDataPB}
@ -75,6 +81,14 @@ export const EditCellWrapper = ({
></CellOptions>
)}
{cellIdentifier.fieldType === FieldType.Checklist && cellController && (
<CheckList
data={data as SelectOptionCellDataPB}
fieldId={cellIdentifier.fieldId}
onEditClick={(left, top) => onEditCheckListClick(cellIdentifier, left, top)}
></CheckList>
)}
{cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
<EditCheckboxCell
data={data as 'Yes' | 'No' | undefined}

View File

@ -1,5 +1,4 @@
import { MouseEventHandler, useEffect, useRef, useState } from 'react';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
import { useTranslation } from 'react-i18next';
@ -11,7 +10,7 @@ import { useAppSelector } from '$app/stores/store';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { FieldType } from '@/services/backend';
import { DateTypeOptions } from '$app/components/_shared/EditRow/DateTypeOptions';
import { DateTypeOptions } from '$app/components/_shared/EditRow/Date/DateTypeOptions';
export const EditFieldPopup = ({
top,
@ -35,7 +34,7 @@ export const EditFieldPopup = ({
onNumberFormat?: (buttonLeft: number, buttonTop: number) => void;
}) => {
const databaseStore = useAppSelector((state) => state.database);
const { t } = useTranslation('');
const { t } = useTranslation();
const changeTypeButtonRef = useRef<HTMLDivElement>(null);
const [name, setName] = useState('');
@ -56,15 +55,6 @@ export const EditFieldPopup = ({
changeFieldTypeClick(buttonTop, buttonRight);
};
// this is causing an error right now
const onDeleteFieldClick = async () => {
if (!fieldInfo) return;
const controller = new TypeOptionController(viewId, Some(fieldInfo));
await controller.initialize();
await controller.deleteField();
onOutsideClick();
};
const onNumberFormatClick: MouseEventHandler = (e) => {
e.stopPropagation();
let target = e.target as HTMLElement;
@ -96,18 +86,6 @@ export const EditFieldPopup = ({
className={'border-shades-3 flex-1 rounded border bg-main-selector px-2 py-2'}
/>
<button
onClick={() => onDeleteFieldClick()}
className={'flex cursor-pointer items-center gap-2 rounded-lg py-2 text-main-alert hover:bg-main-secondary'}
>
<span className={'flex items-center gap-2 pl-2'}>
<i className={'block h-5 w-5'}>
<TrashSvg></TrashSvg>
</i>
<span>{t('grid.field.delete')}</span>
</span>
</button>
<div
ref={changeTypeButtonRef}
onClick={() => onChangeFieldTypeClick()}

View File

@ -12,11 +12,16 @@ import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFiel
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
import { Some } from 'ts-results';
import { FieldType, SelectOptionPB } from '@/services/backend';
import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
import { CellOptionsPopup } from '$app/components/_shared/EditRow/Options/CellOptionsPopup';
import { DatePickerPopup } from '$app/components/_shared/EditRow/Date/DatePickerPopup';
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/EditCellOptionPopup';
import { NumberFormatPopup } from '$app/components/_shared/EditRow/NumberFormatPopup';
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/Options/EditCellOptionPopup';
import { NumberFormatPopup } from '$app/components/_shared/EditRow/Date/NumberFormatPopup';
import { CheckListPopup } from '$app/components/_shared/EditRow/CheckList/CheckListPopup';
import { EditCheckListPopup } from '$app/components/_shared/EditRow/CheckList/EditCheckListPopup';
import { PropertiesPanel } from '$app/components/_shared/EditRow/PropertiesPanel';
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
import { PromptWindow } from '$app/components/_shared/PromptWindow';
export const EditRow = ({
onClose,
@ -30,7 +35,7 @@ export const EditRow = ({
rowInfo: RowInfo;
}) => {
const { cells, onNewColumnClick } = useRow(viewId, controller, rowInfo);
const { t } = useTranslation('');
const { t } = useTranslation();
const [unveil, setUnveil] = useState(false);
const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
@ -56,10 +61,21 @@ export const EditRow = ({
const [editingSelectOption, setEditingSelectOption] = useState<SelectOptionPB | undefined>();
const [showEditCheckList, setShowEditCheckList] = useState(false);
const [editCheckListTop, setEditCheckListTop] = useState(0);
const [editCheckListLeft, setEditCheckListLeft] = useState(0);
const [showNumberFormatPopup, setShowNumberFormatPopup] = useState(false);
const [numberFormatTop, setNumberFormatTop] = useState(0);
const [numberFormatLeft, setNumberFormatLeft] = useState(0);
const [showCheckListPopup, setShowCheckListPopup] = useState(false);
const [checkListPopupTop, setCheckListPopupTop] = useState(0);
const [checkListPopupLeft, setCheckListPopupLeft] = useState(0);
const [deletingPropertyId, setDeletingPropertyId] = useState<string | null>(null);
const [showDeletePropertyPrompt, setShowDeletePropertyPrompt] = useState(false);
useEffect(() => {
setUnveil(true);
}, []);
@ -125,12 +141,26 @@ export const EditRow = ({
setEditCellOptionTop(_top);
};
const onOpenCheckListDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => {
setEditingSelectOption(_select_option);
setShowEditCheckList(true);
setEditCheckListLeft(_left + 10);
setEditCheckListTop(_top);
};
const onNumberFormat = (_left: number, _top: number) => {
setShowNumberFormatPopup(true);
setNumberFormatLeft(_left + 10);
setNumberFormatTop(_top);
};
const onEditCheckListClick = (cellIdentifier: CellIdentifier, left: number, top: number) => {
setEditingCell(cellIdentifier);
setShowCheckListPopup(true);
setCheckListPopupLeft(left);
setCheckListPopupTop(top + 40);
};
const onDragEnd: OnDragEndResponder = (result) => {
if (!result.destination?.index) return;
void controller.moveField({
@ -140,129 +170,193 @@ export const EditRow = ({
});
};
const onDeletePropertyClick = (fieldId: string) => {
setDeletingPropertyId(fieldId);
setShowDeletePropertyPrompt(true);
};
const onDelete = async () => {
if (!deletingPropertyId) return;
const fieldInfo = controller.fieldController.getField(deletingPropertyId);
if (!fieldInfo) return;
const typeController = new TypeOptionController(viewId, Some(fieldInfo));
await typeController.initialize();
await typeController.deleteField();
setShowDeletePropertyPrompt(false);
};
return (
<div
className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${
unveil ? 'opacity-100' : 'opacity-0'
}`}
onClick={() => onCloseClick()}
>
<>
<div
onClick={(e) => {
e.stopPropagation();
}}
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white px-8 pb-4 pt-12`}
className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${
unveil ? 'opacity-100' : 'opacity-0'
}`}
onClick={() => onCloseClick()}
>
<div onClick={() => onCloseClick()} className={'absolute top-4 right-4'}>
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
<CloseSvg></CloseSvg>
</button>
</div>
<div
onClick={(e) => {
e.stopPropagation();
}}
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white `}
>
<div onClick={() => onCloseClick()} className={'absolute top-1 right-1'}>
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
<CloseSvg></CloseSvg>
</button>
</div>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId={'field-list'}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={`flex flex-1 flex-col gap-2 ${
showFieldEditor || showChangeOptionsPopup || showDatePicker ? 'overflow-hidden' : 'overflow-auto'
}`}
>
{cells.map((cell, cellIndex) => (
<EditCellWrapper
index={cellIndex}
key={cellIndex}
cellIdentifier={cell.cellIdentifier}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onEditFieldClick={onEditFieldClick}
onEditOptionsClick={onEditOptionsClick}
onEditDateClick={onEditDateClick}
></EditCellWrapper>
))}
<div className={'flex h-full'}>
<div className={'flex h-full flex-1 flex-col border-r border-shade-6 pb-4 pt-6'}>
<div className={'pl-12 pb-4'}>
<button className={'flex items-center gap-2 p-4'}>
<i className={'h-5 w-5'}>
<ImageSvg></ImageSvg>
</i>
<span className={'text-xs'}>Add Cover</span>
</button>
</div>
)}
</Droppable>
</DragDropContext>
<div className={'border-t border-shade-6 pt-2'}>
<button
onClick={() => onNewColumnClick()}
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-shade-6'}
>
<i className={'h-5 w-5'}>
<AddSvg></AddSvg>
</i>
<span>{t('grid.field.newColumn')}</span>
</button>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId={'field-list'}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={`flex flex-1 flex-col gap-8 px-8 ${
showFieldEditor || showChangeOptionsPopup || showDatePicker ? 'overflow-hidden' : 'overflow-auto'
}`}
>
{cells.map((cell, cellIndex) => (
<EditCellWrapper
index={cellIndex}
key={cellIndex}
cellIdentifier={cell.cellIdentifier}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onEditFieldClick={onEditFieldClick}
onEditOptionsClick={onEditOptionsClick}
onEditDateClick={onEditDateClick}
onEditCheckListClick={onEditCheckListClick}
></EditCellWrapper>
))}
</div>
)}
</Droppable>
</DragDropContext>
<div className={'border-t border-shade-6 px-8 pt-2'}>
<button
onClick={() => onNewColumnClick()}
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-shade-6'}
>
<i className={'h-5 w-5'}>
<AddSvg></AddSvg>
</i>
<span>{t('grid.field.newProperty')}</span>
</button>
</div>
</div>
<PropertiesPanel
viewId={viewId}
controller={controller}
rowInfo={rowInfo}
onDeletePropertyClick={onDeletePropertyClick}
></PropertiesPanel>
</div>
{showFieldEditor && editingCell && (
<EditFieldPopup
top={editFieldTop}
left={editFieldLeft}
cellIdentifier={editingCell}
viewId={viewId}
onOutsideClick={onOutsideEditFieldClick}
fieldInfo={controller.fieldController.getField(editingCell.fieldId)}
fieldController={controller.fieldController}
changeFieldTypeClick={onChangeFieldTypeClick}
onNumberFormat={onNumberFormat}
></EditFieldPopup>
)}
{showChangeFieldTypePopup && (
<ChangeFieldTypePopup
top={changeFieldTypeTop}
left={changeFieldTypeLeft}
onClick={(newType) => changeFieldType(newType)}
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
></ChangeFieldTypePopup>
)}
{showChangeOptionsPopup && editingCell && (
<CellOptionsPopup
top={changeOptionsTop}
left={changeOptionsLeft}
cellIdentifier={editingCell}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onOutsideClick={() => setShowChangeOptionsPopup(false)}
openOptionDetail={onOpenOptionDetailClick}
></CellOptionsPopup>
)}
{showDatePicker && editingCell && (
<DatePickerPopup
top={datePickerTop}
left={datePickerLeft}
cellIdentifier={editingCell}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onOutsideClick={() => setShowDatePicker(false)}
></DatePickerPopup>
)}
{showEditCellOption && editingCell && editingSelectOption && (
<EditCellOptionPopup
top={editCellOptionTop}
left={editCellOptionLeft}
cellIdentifier={editingCell}
editingSelectOption={editingSelectOption}
onOutsideClick={() => {
setShowEditCellOption(false);
}}
></EditCellOptionPopup>
)}
{showNumberFormatPopup && editingCell && (
<NumberFormatPopup
top={numberFormatTop}
left={numberFormatLeft}
cellIdentifier={editingCell}
fieldController={controller.fieldController}
onOutsideClick={() => {
setShowNumberFormatPopup(false);
}}
></NumberFormatPopup>
)}
{showCheckListPopup && editingCell && (
<CheckListPopup
top={checkListPopupTop}
left={checkListPopupLeft}
cellIdentifier={editingCell}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onOutsideClick={() => setShowCheckListPopup(false)}
openCheckListDetail={onOpenCheckListDetailClick}
></CheckListPopup>
)}
{showEditCheckList && editingCell && editingSelectOption && (
<EditCheckListPopup
top={editCheckListTop}
left={editCheckListLeft}
cellIdentifier={editingCell}
editingSelectOption={editingSelectOption}
onOutsideClick={() => setShowEditCheckList(false)}
></EditCheckListPopup>
)}
</div>
{showFieldEditor && editingCell && (
<EditFieldPopup
top={editFieldTop}
left={editFieldLeft}
cellIdentifier={editingCell}
viewId={viewId}
onOutsideClick={onOutsideEditFieldClick}
fieldInfo={controller.fieldController.getField(editingCell.fieldId)}
fieldController={controller.fieldController}
changeFieldTypeClick={onChangeFieldTypeClick}
onNumberFormat={onNumberFormat}
></EditFieldPopup>
)}
{showChangeFieldTypePopup && (
<ChangeFieldTypePopup
top={changeFieldTypeTop}
left={changeFieldTypeLeft}
onClick={(newType) => changeFieldType(newType)}
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
></ChangeFieldTypePopup>
)}
{showChangeOptionsPopup && editingCell && (
<CellOptionsPopup
top={changeOptionsTop}
left={changeOptionsLeft}
cellIdentifier={editingCell}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onOutsideClick={() => setShowChangeOptionsPopup(false)}
openOptionDetail={onOpenOptionDetailClick}
></CellOptionsPopup>
)}
{showDatePicker && editingCell && (
<DatePickerPopup
top={datePickerTop}
left={datePickerLeft}
cellIdentifier={editingCell}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onOutsideClick={() => setShowDatePicker(false)}
></DatePickerPopup>
)}
{showEditCellOption && editingCell && editingSelectOption && (
<EditCellOptionPopup
top={editCellOptionTop}
left={editCellOptionLeft}
cellIdentifier={editingCell}
editingSelectOption={editingSelectOption}
onOutsideClick={() => {
setShowEditCellOption(false);
}}
></EditCellOptionPopup>
)}
{showNumberFormatPopup && editingCell && (
<NumberFormatPopup
top={numberFormatTop}
left={numberFormatLeft}
cellIdentifier={editingCell}
fieldController={controller.fieldController}
onOutsideClick={() => {
setShowNumberFormatPopup(false);
}}
></NumberFormatPopup>
)}
</div>
</div>
{showDeletePropertyPrompt && (
<PromptWindow
msg={'Are you sure you want to delete this property?'}
onYes={() => onDelete()}
onCancel={() => setShowDeletePropertyPrompt(false)}
></PromptWindow>
)}
</>
);
};

View File

@ -2,7 +2,7 @@ import { FieldType } from '@/services/backend';
import { useTranslation } from 'react-i18next';
export const FieldTypeName = ({ fieldType }: { fieldType: FieldType }) => {
const { t } = useTranslation('');
const { t } = useTranslation();
return (
<>
{fieldType === FieldType.RichText && t('grid.field.textFieldName')}

View File

@ -11,7 +11,7 @@ export const EditCellNumber = ({
const [value, setValue] = useState('');
useEffect(() => {
setValue(data || '');
setValue(data ?? '');
}, [data]);
const save = async () => {
@ -23,7 +23,7 @@ export const EditCellNumber = ({
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => save()}
className={'w-full px-4 py-2'}
className={'w-full px-4 py-1'}
></input>
);
};

View File

@ -12,12 +12,12 @@ export const EditCellText = ({
const [contentRows, setContentRows] = useState(1);
useEffect(() => {
setValue(data || '');
setValue(data ?? '');
}, [data]);
useEffect(() => {
if (!value?.length) return;
setContentRows(Math.max(1, (value || '').split('\n').length));
setContentRows(Math.max(1, (value ?? '').split('\n').length));
}, [value]);
const onTextFieldChange = async (v: string) => {
@ -29,9 +29,9 @@ export const EditCellText = ({
};
return (
<div className={''}>
<div>
<textarea
className={'mt-0.5 h-full w-full resize-none px-4 py-2'}
className={'mt-0.5 h-full w-full resize-none px-4 py-1'}
rows={contentRows}
value={value}
onChange={(e) => onTextFieldChange(e.target.value)}

View File

@ -13,7 +13,7 @@ export const EditCellUrl = ({
const [value, setValue] = useState('');
useEffect(() => {
setValue((data as URLCellDataPB)?.url || '');
setValue((data as URLCellDataPB)?.url ?? '');
}, [data]);
const save = async () => {
@ -25,7 +25,7 @@ export const EditCellUrl = ({
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => save()}
className={'w-full px-4 py-2'}
className={'w-full px-4 py-1'}
></input>
);
};

View File

@ -18,7 +18,7 @@ export const EditCheckboxCell = ({
};
return (
<div onClick={() => toggleValue()} className={'block px-4 py-2'}>
<div onClick={() => toggleValue()} className={'block px-4 py-1'}>
<button className={'h-5 w-5'}>
{data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
</button>

View File

@ -0,0 +1,69 @@
import { SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
import { getBgColor } from '$app/components/_shared/getColor';
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
import { ISelectOption } from '$app_reducers/database/slice';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { MouseEventHandler } from 'react';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
export const CellOption = ({
option,
checked,
cellIdentifier,
openOptionDetail,
clearValue,
}: {
option: ISelectOption;
checked: boolean;
cellIdentifier: CellIdentifier;
openOptionDetail: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
clearValue: () => void;
}) => {
const onOptionDetailClick: MouseEventHandler = (e) => {
e.stopPropagation();
let target = e.target as HTMLElement;
while (!(target instanceof HTMLButtonElement)) {
if (target.parentElement === null) return;
target = target.parentElement;
}
const selectOption = new SelectOptionPB({
id: option.selectOptionId,
name: option.title,
color: option.color ?? SelectOptionColorPB.Purple,
});
const { right: _left, top: _top } = target.getBoundingClientRect();
openOptionDetail(_left, _top, selectOption);
};
const onToggleOptionClick: MouseEventHandler = async () => {
if (checked) {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.selectOptionId]);
} else {
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.selectOptionId]);
}
clearValue();
};
return (
<div
onClick={onToggleOptionClick}
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'}
>
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div>
<div className={'flex items-center'}>
{checked && (
<button className={'h-5 w-5 p-1'}>
<CheckmarkSvg></CheckmarkSvg>
</button>
)}
<button onClick={onOptionDetailClick} className={'h-6 w-6 p-1'}>
<Details2Svg></Details2Svg>
</button>
</div>
</div>
);
};

View File

@ -1,6 +1,6 @@
import { SelectOptionCellDataPB } from '@/services/backend';
import { getBgColor } from '$app/components/_shared/getColor';
import { useRef } from 'react';
import { MouseEventHandler, useRef } from 'react';
export const CellOptions = ({
data,
@ -11,23 +11,19 @@ export const CellOptions = ({
}) => {
const ref = useRef<HTMLDivElement>(null);
const onClick = () => {
const onClick: MouseEventHandler = () => {
if (!ref.current) return;
const { left, top } = ref.current.getBoundingClientRect();
onEditClick(left, top);
};
return (
<div
ref={ref}
onClick={() => onClick()}
className={'flex w-full flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'}
>
<div ref={ref} onClick={onClick} className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs text-black'}>
{data?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
{option?.name || ''}
{option?.name ?? ''}
</div>
)) || ''}
))}
&nbsp;
</div>
);

View File

@ -0,0 +1,104 @@
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { SelectOptionCellDataPB, SelectOptionPB } from '@/services/backend';
import { useTranslation } from 'react-i18next';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { useAppSelector } from '$app/stores/store';
import { ISelectOptionType } from '$app_reducers/database/slice';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { CellOption } from '$app/components/_shared/EditRow/Options/CellOption';
import { SelectedOption } from '$app/components/_shared/EditRow/Options/SelectedOption';
export const CellOptionsPopup = ({
top,
left,
cellIdentifier,
cellCache,
fieldController,
onOutsideClick,
openOptionDetail,
}: {
top: number;
left: number;
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
onOutsideClick: () => void;
openOptionDetail: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const [value, setValue] = useState('');
const { data } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database);
useEffect(() => {
if (inputRef?.current) {
inputRef.current.focus();
}
}, [inputRef]);
const onKeyDown: KeyboardEventHandler = async (e) => {
if (e.key === 'Enter' && value.length > 0) {
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
setValue('');
}
};
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
if (e.key === 'Escape') {
onOutsideClick();
}
};
return (
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
<div className={'flex flex-wrap items-center gap-2 text-black'}>
{(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
<SelectedOption
option={option}
key={index}
cellIdentifier={cellIdentifier}
clearValue={() => setValue('')}
></SelectedOption>
))}
</div>
<input
ref={inputRef}
className={'py-2'}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={t('grid.selectOption.searchOption') ?? ''}
onKeyDown={onKeyDown}
/>
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
</div>
<div className={'-mx-4 h-[1px] bg-shade-6'}></div>
<div className={'font-medium text-shade-3'}>{t('grid.selectOption.panelTitle') ?? ''}</div>
<div className={'flex flex-col gap-1'}>
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
(option, index) => (
<CellOption
key={index}
option={option}
checked={
!!(data as SelectOptionCellDataPB)?.select_options?.find(
(selectedOption) => selectedOption.id === option.selectOptionId
)
}
cellIdentifier={cellIdentifier}
openOptionDetail={openOptionDetail}
clearValue={() => setValue('')}
></CellOption>
)
)}
</div>
</div>
</PopupWindow>
);
};

View File

@ -22,7 +22,7 @@ export const EditCellOptionPopup = ({
onOutsideClick: () => void;
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation('');
const { t } = useTranslation();
const [value, setValue] = useState('');
useEffect(() => {

View File

@ -0,0 +1,30 @@
import { getBgColor } from '$app/components/_shared/getColor';
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
import { SelectOptionPB } from '@/services/backend';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { MouseEventHandler } from 'react';
export const SelectedOption = ({
option,
cellIdentifier,
clearValue,
}: {
option: SelectOptionPB;
cellIdentifier: CellIdentifier;
clearValue: () => void;
}) => {
const onUnselectOptionClick: MouseEventHandler = async () => {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
clearValue();
};
return (
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`}>
<span>{option?.name ?? ''}</span>
<button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer'}>
<CloseSvg></CloseSvg>
</button>
</div>
);
};

View File

@ -0,0 +1,169 @@
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
import { useEffect, useState } from 'react';
import { useRow } from '$app/components/_shared/database-hooks/useRow';
import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { useAppSelector } from '$app/stores/store';
import { Switch } from '$app/components/_shared/Switch';
import { FieldType } from '@/services/backend';
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
const typesOrder: FieldType[] = [
FieldType.RichText,
FieldType.Number,
FieldType.DateTime,
FieldType.SingleSelect,
FieldType.MultiSelect,
FieldType.Checkbox,
FieldType.URL,
FieldType.Checklist,
];
export const PropertiesPanel = ({
viewId,
controller,
rowInfo,
onDeletePropertyClick,
}: {
viewId: string;
controller: DatabaseController;
rowInfo: RowInfo;
onDeletePropertyClick: (fieldId: string) => void;
}) => {
const { cells } = useRow(viewId, controller, rowInfo);
const databaseStore = useAppSelector((state) => state.database);
const [showAddedProperties, setShowAddedProperties] = useState(true);
const [showBasicProperties, setShowBasicProperties] = useState(false);
const [showAdvancedProperties, setShowAdvancedProperties] = useState(false);
const [hoveredPropertyIndex, setHoveredPropertyIndex] = useState(-1);
const [hiddenProperties, setHiddenProperties] = useState<boolean[]>([]);
useEffect(() => {
setHiddenProperties(cells.map(() => false));
}, [cells]);
const toggleHideProperty = (v: boolean, index: number) => {
setHiddenProperties(hiddenProperties.map((h, i) => (i === index ? !v : h)));
};
return (
<div className={'flex flex-col gap-2 overflow-auto py-12 px-4'}>
<div
onClick={() => setShowAddedProperties(!showAddedProperties)}
className={'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-shade-6'}
>
<div className={'text-sm'}>Added Properties</div>
<i className={`h-5 w-5 transition-transform duration-500 ${showAddedProperties && 'rotate-180'}`}>
<DropDownShowSvg></DropDownShowSvg>
</i>
</div>
<div className={'flex flex-col text-xs'} onMouseLeave={() => setHoveredPropertyIndex(-1)}>
{showAddedProperties &&
cells.map((cell, cellIndex) => (
<div
key={cellIndex}
onMouseEnter={() => setHoveredPropertyIndex(cellIndex)}
className={
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2 py-1 hover:bg-main-secondary'
}
>
<div className={'flex items-center gap-2 text-black'}>
<div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center'}>
<FieldTypeIcon fieldType={cell.cellIdentifier.fieldType}></FieldTypeIcon>
</div>
<span className={'overflow-hidden text-ellipsis whitespace-nowrap'}>
{databaseStore.fields[cell.cellIdentifier.fieldId]?.title ?? ''}
</span>
</div>
<div className={'flex items-center'}>
<i
onClick={() => onDeletePropertyClick(cell.cellIdentifier.fieldId)}
className={`h-[16px] w-[16px] text-black transition-opacity duration-300 ${
hoveredPropertyIndex === cellIndex ? 'opacity-100' : 'opacity-0'
}`}
>
<TrashSvg></TrashSvg>
</i>
<Switch value={!hiddenProperties[cellIndex]} setValue={(v) => toggleHideProperty(v, cellIndex)}></Switch>
</div>
</div>
))}
</div>
<div
onClick={() => setShowBasicProperties(!showBasicProperties)}
className={'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-shade-6'}
>
<div className={'text-sm'}>Basic Properties</div>
<i className={`h-5 w-5 transition-transform duration-500 ${showBasicProperties && 'rotate-180'}`}>
<DropDownShowSvg></DropDownShowSvg>
</i>
</div>
<div className={'flex flex-col gap-2 text-xs'}>
{showBasicProperties && (
<div className={'flex flex-col'}>
{typesOrder.map((t, i) => (
<button
onClick={() => console.log('type clicked')}
key={i}
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-main-secondary'}
>
<i className={'h-5 w-5'}>
<FieldTypeIcon fieldType={t}></FieldTypeIcon>
</i>
<span>
<FieldTypeName fieldType={t}></FieldTypeName>
</span>
</button>
))}
</div>
)}
</div>
<div
onClick={() => setShowAdvancedProperties(!showAdvancedProperties)}
className={'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-shade-6'}
>
<div className={'text-sm'}>Advanced Properties</div>
<i className={`h-5 w-5 transition-transform duration-500 ${showAdvancedProperties && 'rotate-180'}`}>
<DropDownShowSvg></DropDownShowSvg>
</i>
</div>
<div className={'flex flex-col gap-2 text-xs'}>
{showAdvancedProperties && (
<div className={'flex flex-col'}>
<button
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-main-secondary'}
>
<i className={'h-5 w-5'}>
<MultiSelectTypeSvg></MultiSelectTypeSvg>
</i>
<span>Last edited time</span>
</button>
<button
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-main-secondary'}
>
<i className={'h-5 w-5'}>
<DocumentSvg></DocumentSvg>
</i>
<span>Document</span>
</button>
<button
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-main-secondary'}
>
<i className={'h-5 w-5'}>
<SingleSelectTypeSvg></SingleSelectTypeSvg>
</i>
<span>Relation to</span>
</button>
</div>
)}
</div>
</div>
);
};

View File

@ -9,7 +9,7 @@ export const PopupWindow = ({
top,
}: {
children: ReactNode;
className: string;
className?: string;
onOutsideClick: () => void;
left: number;
top: number;
@ -33,7 +33,7 @@ export const PopupWindow = ({
} else {
setAdjustedLeft(left);
}
}, [ref, left, top, window]);
}, [ref, left, top]);
return (
<div
@ -41,7 +41,7 @@ export const PopupWindow = ({
className={
'fixed z-10 rounded-lg bg-white shadow-md transition-opacity duration-300 ' +
(adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
(className || '')
(className ?? '')
}
style={{ top: `${adjustedTop}px`, left: `${adjustedLeft}px` }}
>

View File

@ -0,0 +1,24 @@
import { Button } from '$app/components/_shared/Button';
export const PromptWindow = ({ msg, onYes, onCancel }: { msg: string; onYes: () => void; onCancel: () => void }) => {
return (
<div
className='fixed inset-0 z-20 flex items-center justify-center bg-black/30 backdrop-blur-sm'
onClick={() => onCancel()}
>
<div className={'rounded-xl bg-white p-16'} onClick={(e) => e.stopPropagation()}>
<div className={'flex flex-col items-center justify-center gap-8'}>
<div className={'text-black'}>{msg}</div>
<div className={'flex items-center justify-around gap-4'}>
<Button onClick={() => onCancel()} size={'medium-transparent'}>
Cancel
</Button>
<Button onClick={() => onYes()} size={'medium'}>
Yes
</Button>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,8 @@
export const Switch = ({ value, setValue }: { value: boolean; setValue: (v: boolean) => void }) => {
return (
<label className='form-switch' style={{ transform: 'scale(0.5)', marginRight: '-16px' }}>
<input type='checkbox' checked={value} onChange={() => setValue(!value)} />
<i></i>
</label>
);
};

View File

@ -0,0 +1,12 @@
export const DragElementSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect x='9' y='3' width='2' height='2' rx='0.5' fill='currentColor' />
<rect x='5' y='3' width='2' height='2' rx='0.5' fill='currentColor' />
<rect x='9' y='7' width='2' height='2' rx='0.5' fill='currentColor' />
<rect x='5' y='7' width='2' height='2' rx='0.5' fill='currentColor' />
<rect x='9' y='11' width='2' height='2' rx='0.5' fill='currentColor' />
<rect x='5' y='11' width='2' height='2' rx='0.5' fill='currentColor' />
</svg>
);
};

View File

@ -0,0 +1,9 @@
export const ImageSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect x='1.5' y='3' width='13' height='10' rx='1.5' stroke='currentColor' />
<circle cx='5.5' cy='6.5' r='1' stroke='currentColor' />
<path d='M5 13L10.112 8.45603C10.4211 8.18126 10.8674 8.12513 11.235 8.31482L14.5 10' stroke='currentColor' />
</svg>
);
};

View File

@ -9,11 +9,10 @@ import { EarthSvg } from '../../_shared/svg/EarthSvg';
import { useState } from 'react';
import { LanguageSelectPopup } from '../../_shared/LanguageSelectPopup';
export const Login = () => {
const { showPassword, onTogglePassword, onSignInClick, email, setEmail, password, setPassword, authError } =
useLogin();
const { t } = useTranslation('');
const { t } = useTranslation();
const [showLanguagePopup, setShowLanguagePopup] = useState(false);
return (
@ -34,17 +33,17 @@ export const Login = () => {
<input
type='text'
className={`input w-full ${authError && 'error'}`}
placeholder={t('signIn.emailHint') || ''}
placeholder={t('signIn.emailHint') ?? ''}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<div className='relative w-full'>
{/* Password input field */}
<input
type={showPassword ? 'text' : 'password'}
className={`input w-full !pr-10 ${authError && 'error'}`}
placeholder={t('signIn.passwordHint') || ''}
placeholder={t('signIn.passwordHint') ?? ''}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>

View File

@ -27,7 +27,7 @@ export const SignUp = () => {
setRepeatedPassword,
authError,
} = useSignUp();
const { t } = useTranslation('');
const { t } = useTranslation();
const [showLanguagePopup, setShowLanguagePopup] = useState(false);
return (
@ -45,7 +45,7 @@ export const SignUp = () => {
<input
type='text'
className={`input w-full ${authError && 'error'}`}
placeholder={t('signUp.emailHint') || ''}
placeholder={t('signUp.emailHint') ?? ''}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
@ -61,7 +61,7 @@ export const SignUp = () => {
<input
type={showPassword ? 'text' : 'password'}
className={`input w-full !pr-10 ${authError && 'error'}`}
placeholder={t('signUp.passwordHint') || ''}
placeholder={t('signUp.passwordHint') ?? ''}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
@ -79,7 +79,7 @@ export const SignUp = () => {
<input
type={showConfirmPassword ? 'text' : 'password'}
className={`input w-full !pr-10 ${authError && 'error'}`}
placeholder={t('signUp.repeatPasswordHint') || ''}
placeholder={t('signUp.repeatPasswordHint') ?? ''}
value={repeatedPassword}
onChange={(e) => setRepeatedPassword(e.target.value)}
/>

View File

@ -1,6 +1,5 @@
import { SearchInput } from '../_shared/SearchInput';
import { BoardGroup } from './BoardGroup';
import { NewBoardBlock } from './NewBoardBlock';
import { useDatabase } from '../_shared/database-hooks/useDatabase';
import { ViewLayoutPB } from '@/services/backend';
import { DragDropContext } from 'react-beautiful-dnd';
@ -45,7 +44,6 @@ export const Board = ({ viewId, title }: { viewId: string; title: string }) => {
onOpenRow={onOpenRow}
/>
))}
<NewBoardBlock onClick={() => console.log('new block')}></NewBoardBlock>
</div>
</div>
</DragDropContext>

View File

@ -8,6 +8,7 @@ import { MouseEventHandler, useState } from 'react';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
import { useTranslation } from 'react-i18next';
export const BoardCard = ({
index,
@ -24,6 +25,8 @@ export const BoardCard = ({
groupByFieldId: string;
onOpenRow: (rowId: RowInfo) => void;
}) => {
const { t } = useTranslation();
const { cells } = useRow(viewId, controller, rowInfo);
const [showCardPopup, setShowCardPopup] = useState(false);
@ -95,7 +98,7 @@ export const BoardCard = ({
<i className={'h-5 w-5'}>
<TrashSvg></TrashSvg>
</i>
<span className={'flex-shrink-0'}>Delete</span>
<span className={'flex-shrink-0'}>{t('grid.row.delete')}</span>
</button>
</PopupWindow>
)}

View File

@ -7,6 +7,7 @@ import { BoardDateCell } from './BoardDateCell';
import { BoardTextCell } from './BoardTextCell';
import { BoardUrlCell } from '$app/components/board/BoardUrlCell';
import { BoardCheckboxCell } from '$app/components/board/BoardCheckboxCell';
import { BoardCheckListCell } from '$app/components/board/BoardCheckListCell';
export const BoardCell = ({
cellIdentifier,
@ -19,14 +20,18 @@ export const BoardCell = ({
}) => {
return (
<>
{cellIdentifier.fieldType === FieldType.SingleSelect ||
cellIdentifier.fieldType === FieldType.MultiSelect ||
cellIdentifier.fieldType === FieldType.Checklist ? (
{cellIdentifier.fieldType === FieldType.SingleSelect || cellIdentifier.fieldType === FieldType.MultiSelect ? (
<BoardOptionsCell
cellIdentifier={cellIdentifier}
cellCache={cellCache}
fieldController={fieldController}
></BoardOptionsCell>
) : cellIdentifier.fieldType === FieldType.Checklist ? (
<BoardCheckListCell
cellIdentifier={cellIdentifier}
cellCache={cellCache}
fieldController={fieldController}
></BoardCheckListCell>
) : cellIdentifier.fieldType === FieldType.DateTime ? (
<BoardDateCell
cellIdentifier={cellIdentifier}

View File

@ -0,0 +1,37 @@
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { useEffect, useState } from 'react';
import { ISelectOptionType } from '$app_reducers/database/slice';
import { SelectOptionCellDataPB } from '@/services/backend';
import { useAppSelector } from '$app/stores/store';
import { CheckListProgress } from '$app/components/_shared/CheckListProgress';
export const BoardCheckListCell = ({
cellIdentifier,
cellCache,
fieldController,
}: {
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
}) => {
const { data } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database);
const [allOptionsCount, setAllOptionsCount] = useState(0);
const [selectedOptionsCount, setSelectedOptionsCount] = useState(0);
useEffect(() => {
setAllOptionsCount(
(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType)?.selectOptions?.length ?? 0
);
}, [databaseStore, cellIdentifier]);
useEffect(() => {
setSelectedOptionsCount((data as SelectOptionCellDataPB)?.select_options?.length ?? 0);
}, [data]);
return <CheckListProgress completed={selectedOptionsCount} max={allOptionsCount} />;
};

View File

@ -14,5 +14,5 @@ export const BoardDateCell = ({
fieldController: FieldController;
}) => {
const { data } = useCell(cellIdentifier, cellCache, fieldController);
return <div>{(data as DateCellDataPB | undefined)?.date || ''}</div>;
return <div>{(data as DateCellDataPB | undefined)?.date ?? ''}&nbsp;</div>;
};

View File

@ -5,6 +5,7 @@ import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { Droppable } from 'react-beautiful-dnd';
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
import { useTranslation } from 'react-i18next';
export const BoardGroup = ({
viewId,
@ -23,6 +24,8 @@ export const BoardGroup = ({
onOpenRow: (rowId: RowInfo) => void;
group: DatabaseGroupController;
}) => {
const { t } = useTranslation();
return (
<div className={'flex h-full w-[250px] flex-col rounded-lg bg-surface-1'}>
<div className={'flex items-center justify-between p-4'}>
@ -73,7 +76,7 @@ export const BoardGroup = ({
<span className={'h-5 w-5'}>
<AddSvg></AddSvg>
</span>
<span>New</span>
<span>{t('board.column.create_new_card')}</span>
</button>
</div>
</div>

View File

@ -20,9 +20,9 @@ export const BoardOptionsCell = ({
<div className={'flex flex-wrap items-center gap-2 py-2 text-xs text-black'}>
{(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
{option?.name || ''}
{option?.name ?? ''}
</div>
)) || ''}
))}
&nbsp;
</div>
);

View File

@ -14,7 +14,7 @@ export const BoardSettingsPopup = ({
onGroupClick: () => void;
}) => {
const [settingsItems, setSettingsItems] = useState<IPopupItem[]>([]);
const { t } = useTranslation('');
const { t } = useTranslation();
useEffect(() => {
setSettingsItems([
{

View File

@ -16,9 +16,10 @@ export const BoardTextCell = ({
return (
<div>
{((data as string | undefined) || '').split('\n').map((line, index) => (
{((data as string | undefined) ?? '').split('\n').map((line, index) => (
<div key={index}>{line}</div>
))}
&nbsp;
</div>
);
};

View File

@ -17,8 +17,8 @@ export const BoardUrlCell = ({
return (
<>
<a className={'text-main-accent hover:underline'} href={(data as URLCellDataPB)?.url || ''} target={'_blank'}>
{(data as URLCellDataPB)?.content || ''}
<a className={'text-main-accent hover:underline'} href={(data as URLCellDataPB)?.url ?? ''} target={'_blank'}>
{(data as URLCellDataPB)?.content ?? ''}&nbsp;
</a>
</>
);

View File

@ -1,14 +0,0 @@
import AddSvg from '../_shared/svg/AddSvg';
export const NewBoardBlock = ({ onClick }: { onClick: () => void }) => {
return (
<div className={'w-[250px]'}>
<button onClick={onClick} className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-surface-2'}>
<span className={'h-5 w-5'}>
<AddSvg></AddSvg>
</span>
<span>Add Block</span>
</button>
</div>
);
};

View File

@ -1,7 +1,7 @@
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
import { EditCheckboxCell } from '../../_shared/EditRow/EditCheckboxCell';
import { EditCheckboxCell } from '../../_shared/EditRow/InlineEditFields/EditCheckboxCell';
import { useCell } from '../../_shared/database-hooks/useCell';
export const GridCheckBox = ({

View File

@ -3,9 +3,9 @@ import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cach
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
import { useCell } from '../../_shared/database-hooks/useCell';
import { DateCellDataPB } from '@/services/backend';
import { EditCellDate } from '../../_shared/EditRow/EditCellDate';
import { EditCellDate } from '../../_shared/EditRow/Date/EditCellDate';
import { useState } from 'react';
import { DatePickerPopup } from '../../_shared/EditRow/DatePickerPopup';
import { DatePickerPopup } from '../../_shared/EditRow/Date/DatePickerPopup';
export const GridDate = ({
cellIdentifier,

View File

@ -2,7 +2,7 @@ import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
import { useCell } from '../../_shared/database-hooks/useCell';
import { EditCellNumber } from '../../_shared/EditRow/EditCellNumber';
import { EditCellNumber } from '../../_shared/EditRow/InlineEditFields/EditCellNumber';
export const GridNumberCell = ({
cellIdentifier,

View File

@ -1,11 +1,11 @@
import { useState } from 'react';
import { CellOptions } from '$app/components/_shared/EditRow/CellOptions';
import { CellOptions } from '$app/components/_shared/EditRow/Options/CellOptions';
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/EditCellOptionPopup';
import { CellOptionsPopup } from '$app/components/_shared/EditRow/Options/CellOptionsPopup';
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/Options/EditCellOptionPopup';
import { SelectOptionCellDataPB, SelectOptionPB } from '@/services/backend';
export default function GridSingleSelectOptions({

View File

@ -2,7 +2,7 @@ import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
import { useCell } from '../../_shared/database-hooks/useCell';
import { EditCellText } from '../../_shared/EditRow/EditCellText';
import { EditCellText } from '../../_shared/EditRow/InlineEditFields/EditCellText';
export default function GridTextCell({
cellIdentifier,

View File

@ -2,7 +2,7 @@ import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
import { useCell } from '../../_shared/database-hooks/useCell';
import { EditCellUrl } from '../../_shared/EditRow/EditCellUrl';
import { EditCellUrl } from '../../_shared/EditRow/InlineEditFields/EditCellUrl';
import { URLCellDataPB } from '@/services/backend';
export const GridUrl = ({

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
export const GridTableHeader = ({ controller }: { controller: DatabaseController }) => {
const { fields, onAddField } = useGridTableHeaderHooks(controller);
const { t } = useTranslation('');
const { t } = useTranslation();
return (
<>

View File

@ -4,7 +4,7 @@ import { useGridAddRow } from './GridAddRow.hooks';
import { useTranslation } from 'react-i18next';
export const GridAddRow = ({ controller }: { controller: DatabaseController }) => {
const { addRow } = useGridAddRow(controller);
const { t } = useTranslation('');
const { t } = useTranslation();
return (
<div>

View File

@ -21,8 +21,8 @@ export const Breadcrumbs = ({ menuHidden, onShowMenuClick }: { menuHidden: boole
useEffect(() => {
const page = pagesStore.find((p) => p.id === activePageId);
const folder = foldersStore.find((f) => f.id === page?.folderId);
setFolderName(folder?.title || '');
setPageName(page?.title || '');
setFolderName(folder?.title ?? '');
setPageName(page?.title ?? '');
}, [pagesStore, foldersStore, activePageId]);
return (

View File

@ -1,12 +1,12 @@
import { foldersActions, IFolder } from '../../../stores/reducers/folders/slice';
import { foldersActions, IFolder } from '$app_reducers/folders/slice';
import { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../../stores/store';
import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { IPage, pagesActions } from '$app_reducers/pages/slice';
import { ViewLayoutPB } from '@/services/backend';
import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc';
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
import { AppObserver } from '$app/stores/effects/folder/app/app_observer';
import { useNavigate } from 'react-router-dom';
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
@ -32,7 +32,7 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
// Backend services
const appBackendService = new AppBackendService(folder.id);
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
useEffect(() => {
void appObserver.subscribe({

View File

@ -2,9 +2,9 @@ import { Details2Svg } from '../../_shared/svg/Details2Svg';
import AddSvg from '../../_shared/svg/AddSvg';
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
import { NewPagePopup } from './NewPagePopup';
import { IFolder } from '../../../stores/reducers/folders/slice';
import { IFolder } from '$app_reducers/folders/slice';
import { useFolderEvents } from './FolderItem.hooks';
import { IPage } from '../../../stores/reducers/pages/slice';
import { IPage } from '$app_reducers/pages/slice';
import { PageItem } from './PageItem';
import { Button } from '../../_shared/Button';
import { RenamePopup } from './RenamePopup';

View File

@ -5,7 +5,7 @@ import { WorkspaceBackendService } from '../../../stores/effects/folder/workspac
export const useNewFolder = () => {
const appDispatch = useAppDispatch();
const workspace = useAppSelector((state) => state.workspace);
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
const onNewFolder = async () => {
const newApp = await workspaceBackendService.createApp({

View File

@ -1,10 +1,10 @@
import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
import { useAppDispatch } from '../../../stores/store';
import { IPage, pagesActions } from '$app_reducers/pages/slice';
import { useAppDispatch } from '$app/stores/store';
import { useEffect, useState } from 'react';
import { nanoid } from 'nanoid';
import { ViewBackendService } from '../../../stores/effects/folder/view/view_bd_svc';
import { useError } from '../../error/Error.hooks';
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
import { useLocation } from 'react-router-dom';
import { ViewPB } from '@/services/backend';
export const usePageEvents = (page: IPage) => {
const appDispatch = useAppDispatch();
@ -41,8 +41,9 @@ export const usePageEvents = (page: IPage) => {
appDispatch(pagesActions.deletePage({ id: page.id }));
};
const duplicatePage = () => {
const duplicatePage = async () => {
closePopup();
await viewBackendService.duplicate(ViewPB.fromObject({}));
appDispatch(
pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
);

View File

@ -9,7 +9,7 @@ export const useWorkspace = () => {
const appDispatch = useAppDispatch();
const userBackendService: UserBackendService = new UserBackendService(currentUser.id || 0);
const userBackendService: UserBackendService = new UserBackendService(currentUser.id ?? 0);
const loadWorkspaceItems = async () => {
try {

View File

@ -37,6 +37,8 @@ import { SortSvg } from '$app/components/_shared/svg/SortSvg';
import { TextTypeSvg } from '$app/components/_shared/svg/TextTypeSvg';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { UrlTypeSvg } from '$app/components/_shared/svg/UrlTypeSvg';
import { DragElementSvg } from '$app/components/_shared/svg/DragElementSvg';
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
export const AllIcons = () => {
return (
@ -83,6 +85,9 @@ export const AllIcons = () => {
<i className={'h-5 w-5'} title={'DocumentSvg'}>
<DocumentSvg></DocumentSvg>
</i>
<i className={'h-5 w-5'} title={'DragElementSvg'}>
<DragElementSvg></DragElementSvg>
</i>
<i className={'h-5 w-5'} title={'DropDownShowSvg'}>
<DropDownShowSvg></DropDownShowSvg>
</i>
@ -116,6 +121,9 @@ export const AllIcons = () => {
<i className={'h-5 w-5'} title={'HideMenuSvg'}>
<HideMenuSvg></HideMenuSvg>
</i>
<i className={'h-5 w-5'} title={'ImageSvg'}>
<ImageSvg></ImageSvg>
</i>
<i className={'h-5 w-5'} title={'InformationSvg'}>
<InformationSvg></InformationSvg>
</i>

View File

@ -5,5 +5,6 @@ import './styles/tailwind.css';
import './styles/font.css';
import './styles/template.css';
import './styles/Calendar.css';
import './styles/switch.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);

View File

@ -0,0 +1,58 @@
.form-switch {
display: inline-block;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.form-switch i {
position: relative;
display: inline-block;
margin-right: 0.5rem;
width: 46px;
height: 26px;
@apply bg-shade-6;
border-radius: 23px;
vertical-align: text-bottom;
transition: all 0.3s linear;
}
.form-switch i::before {
content: '';
position: absolute;
left: 0;
width: 42px;
height: 22px;
background-color: #fff;
border-radius: 11px;
transform: translate3d(2px, 2px, 0) scale3d(1, 1, 1);
transition: all 0.25s linear;
}
.form-switch i::after {
content: '';
position: absolute;
left: 0;
width: 22px;
height: 22px;
background-color: #fff;
border-radius: 11px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24);
transform: translate3d(2px, 2px, 0);
transition: all 0.2s ease-in-out;
}
.form-switch:active i::after {
width: 28px;
transform: translate3d(2px, 2px, 0);
}
.form-switch:active input:checked + i::after {
transform: translate3d(16px, 2px, 0);
}
.form-switch input {
display: none;
}
.form-switch input:checked + i {
@apply bg-main-accent;
}
.form-switch input:checked + i::before {
transform: translate3d(18px, 2px, 0) scale3d(0, 0, 0);
}
.form-switch input:checked + i::after {
transform: translate3d(22px, 2px, 0);
}