diff --git a/playwright/e2e/database/board-column-color.spec.ts b/playwright/e2e/database/board-column-color.spec.ts new file mode 100644 index 000000000..e9678331f --- /dev/null +++ b/playwright/e2e/database/board-column-color.spec.ts @@ -0,0 +1,324 @@ +import { expect, test, type APIRequestContext, type Locator, type Page } from '@playwright/test'; + +import { + BoardSelectors, + DatabaseGridSelectors, + DatabaseViewSelectors, + GridFieldSelectors, + PropertyMenuSelectors, + SingleSelectSelectors, +} from '../../support/selectors'; +import { signInAndCreateDatabaseView } from '../../support/database-ui-helpers'; +import { generateRandomEmail, setupPageErrorHandling } from '../../support/test-config'; +import { waitForDatabaseDocReady } from '../../support/yjs-inject-helpers'; + +type SelectOptionColor = 'Blue' | 'Lime'; + +interface SelectOptionInfo { + fieldId: string; + optionId: string; + color: string; +} + +const colorTargets: Record = { + Blue: { + boardBackgroundVar: '--block-bg-color-12', + selectOptionFillVar: '--tag-fill-09-light', + }, + Lime: { + boardBackgroundVar: '--block-bg-color-6', + selectOptionFillVar: '--tag-fill-06-light', + }, +}; + +test.describe('Board column color', () => { + test.beforeEach(async ({ page }) => { + setupPageErrorHandling(page); + await page.setViewportSize({ width: 1280, height: 720 }); + }); + + test('updates the board group color when the backing select option color changes', async ({ page, request }) => { + const testEmail = generateRandomEmail(); + + await createBoardAndWait(page, request, testEmail); + await waitForDatabaseDocReady(page); + + await enableBoardColorColumnsViaYjs(page); + + const todoOption = await findSelectOptionByNameViaYjs(page, 'To Do'); + const targetColor: SelectOptionColor = todoOption.color === 'Blue' ? 'Lime' : 'Blue'; + + await updateSelectOptionColorViaYjs(page, todoOption.optionId, targetColor); + + await expect + .poll(() => findSelectOptionByIdViaYjs(page, todoOption.optionId).then((option) => option.color)) + .toBe(targetColor); + + const column = BoardSelectors.boardContainer(page).locator(`[data-column-id="${todoOption.optionId}"]`); + const columnSurface = getColumnSurface(column); + const expectedBackgroundColor = await resolveCssColor(page, colorTargets[targetColor].boardBackgroundVar); + + await expect(column).toBeVisible({ timeout: 15000 }); + await expect.poll(() => getComputedBackgroundColor(columnSurface)).toBe(expectedBackgroundColor); + }); + + test('updates the board group color after editing the shared option color from a grid view', async ({ + page, + request, + }) => { + const testEmail = generateRandomEmail(); + + await createBoardAndWait(page, request, testEmail); + await waitForDatabaseDocReady(page); + await enableBoardColorColumnsViaYjs(page); + + const todoOption = await findSelectOptionByNameViaYjs(page, 'To Do'); + const targetColor: SelectOptionColor = todoOption.color === 'Blue' ? 'Lime' : 'Blue'; + const target = colorTargets[targetColor]; + + await addDatabaseViewFromTabBar(page, 'Grid'); + await expect(DatabaseGridSelectors.grid(page)).toBeVisible({ timeout: 15000 }); + + await openGridSelectOptionEditor(page, todoOption); + await selectOptionMenuColor(page, target.selectOptionFillVar); + + await expect + .poll(() => findSelectOptionByIdViaYjs(page, todoOption.optionId).then((option) => option.color)) + .toBe(targetColor); + + await closeOpenMenus(page); + await switchDatabaseViewByName(page, 'Board'); + + const column = BoardSelectors.boardContainer(page).locator(`[data-column-id="${todoOption.optionId}"]`); + const columnSurface = getColumnSurface(column); + const expectedBackgroundColor = await resolveCssColor(page, target.boardBackgroundVar); + + await expect(column).toBeVisible({ timeout: 15000 }); + await expect.poll(() => getComputedBackgroundColor(columnSurface)).toBe(expectedBackgroundColor); + }); +}); + +async function createBoardAndWait(page: Page, request: APIRequestContext, testEmail: string) { + await signInAndCreateDatabaseView(page, request, testEmail, 'Board', { + verify: async (p) => { + await expect(BoardSelectors.boardContainer(p)).toBeVisible({ timeout: 15000 }); + await expect(BoardSelectors.boardContainer(p).locator('[data-column-id]').first()).toBeVisible({ + timeout: 15000, + }); + await expect(BoardSelectors.boardContainer(p).getByText('To Do')).toBeVisible({ timeout: 15000 }); + }, + }); +} + +async function enableBoardColorColumnsViaYjs(page: Page): Promise { + await page.evaluate(() => { + const win = window as any; + const doc = win.__TEST_DATABASE_DOC__; + const Y = win.Y; + const sharedRoot = doc.getMap('data'); + const database = sharedRoot.get('database'); + const view = database.get('views').get(win.__TEST_DATABASE_VIEW_ID__); + + doc.transact(() => { + let layoutSettings = view.get('layout_settings'); + + if (!layoutSettings) { + layoutSettings = new Y.Map(); + view.set('layout_settings', layoutSettings); + } + + let boardLayoutSettings = layoutSettings.get('1'); + + if (!boardLayoutSettings) { + boardLayoutSettings = new Y.Map(); + layoutSettings.set('1', boardLayoutSettings); + } + + boardLayoutSettings.set('show_color_columns', true); + }, 'remote'); + }); +} + +async function findSelectOptionByNameViaYjs(page: Page, optionName: string): Promise { + const option = await page.evaluate((name) => { + const win = window as any; + const doc = win.__TEST_DATABASE_DOC__; + const sharedRoot = doc.getMap('data'); + const database = sharedRoot.get('database'); + const fields = database.get('fields'); + let result: SelectOptionInfo | undefined; + + fields.forEach((field: any, fieldId: string) => { + if (result) return; + + const fieldType = String(field.get('ty')); + const typeOption = field.get('type_option')?.get(fieldType); + const content = typeOption?.get('content'); + + if (!content) return; + + const parsed = JSON.parse(content) as { options?: Array<{ id: string; name: string; color: string }> }; + const selectOption = parsed.options?.find((option) => option.name === name); + + if (!selectOption) return; + + result = { + fieldId, + optionId: selectOption.id, + color: selectOption.color, + }; + }); + + return result; + }, optionName); + + if (!option) { + throw new Error(`Select option "${optionName}" was not found`); + } + + return option; +} + +async function findSelectOptionByIdViaYjs(page: Page, optionId: string): Promise { + const option = await page.evaluate((id) => { + const win = window as any; + const doc = win.__TEST_DATABASE_DOC__; + const sharedRoot = doc.getMap('data'); + const database = sharedRoot.get('database'); + const fields = database.get('fields'); + let result: SelectOptionInfo | undefined; + + fields.forEach((field: any, fieldId: string) => { + if (result) return; + + const fieldType = String(field.get('ty')); + const typeOption = field.get('type_option')?.get(fieldType); + const content = typeOption?.get('content'); + + if (!content) return; + + const parsed = JSON.parse(content) as { options?: Array<{ id: string; color: string }> }; + const selectOption = parsed.options?.find((option) => option.id === id); + + if (!selectOption) return; + + result = { + fieldId, + optionId: selectOption.id, + color: selectOption.color, + }; + }); + + return result; + }, optionId); + + if (!option) { + throw new Error(`Select option "${optionId}" was not found`); + } + + return option; +} + +async function updateSelectOptionColorViaYjs(page: Page, optionId: string, color: SelectOptionColor): Promise { + await page.evaluate( + ({ optionId, color }) => { + const win = window as any; + const doc = win.__TEST_DATABASE_DOC__; + const sharedRoot = doc.getMap('data'); + const database = sharedRoot.get('database'); + const fields = database.get('fields'); + let updated = false; + + fields.forEach((field: any) => { + if (updated) return; + + const fieldType = String(field.get('ty')); + const typeOption = field.get('type_option')?.get(fieldType); + const content = typeOption?.get('content'); + + if (!content) return; + + const parsed = JSON.parse(content) as { options?: Array<{ id: string; color: string }> }; + const options = parsed.options ?? []; + + if (!options.some((option) => option.id === optionId)) return; + + doc.transact(() => { + typeOption.set( + 'content', + JSON.stringify({ + ...parsed, + options: options.map((option) => (option.id === optionId ? { ...option, color } : option)), + }) + ); + }, 'remote'); + + updated = true; + }); + + if (!updated) { + throw new Error(`Select option "${optionId}" was not found`); + } + }, + { optionId, color } + ); +} + +async function addDatabaseViewFromTabBar(page: Page, viewType: 'Grid' | 'Board' | 'Calendar' | 'Chart'): Promise { + await DatabaseViewSelectors.addViewButton(page).click({ force: true }); + + const menu = page.locator('[data-slot="dropdown-menu-content"]').last(); + + await expect(menu).toBeVisible({ timeout: 5000 }); + await menu.getByRole('menuitem', { name: new RegExp(`^${viewType}$`, 'i') }).click({ force: true }); +} + +async function openGridSelectOptionEditor(page: Page, option: SelectOptionInfo): Promise { + await GridFieldSelectors.fieldHeader(page, option.fieldId).last().click({ force: true }); + await expect(PropertyMenuSelectors.editPropertyMenuItem(page).first()).toBeVisible({ timeout: 10000 }); + await PropertyMenuSelectors.editPropertyMenuItem(page).first().click({ force: true }); + await expect(SingleSelectSelectors.selectOption(page, option.optionId)).toBeVisible({ timeout: 10000 }); + await SingleSelectSelectors.selectOption(page, option.optionId).click({ force: true }); +} + +async function selectOptionMenuColor(page: Page, fillVar: string): Promise { + const menu = page.locator('[data-slot="dropdown-menu-content"]').last(); + const colorTile = menu.locator(`div[style*="${fillVar}"]`).first(); + + await expect(colorTile).toBeVisible({ timeout: 5000 }); + await colorTile.click({ force: true }); +} + +async function closeOpenMenus(page: Page): Promise { + await page.keyboard.press('Escape'); + await page.keyboard.press('Escape'); +} + +async function switchDatabaseViewByName(page: Page, viewName: string): Promise { + await DatabaseViewSelectors.viewTab(page).filter({ hasText: viewName }).first().click({ force: true }); + await expect(BoardSelectors.boardContainer(page)).toBeVisible({ timeout: 15000 }); +} + +function getColumnSurface(column: Locator): Locator { + return column.locator(':scope > div').first(); +} + +async function getComputedBackgroundColor(locator: Locator): Promise { + return locator.evaluate((element) => getComputedStyle(element).backgroundColor); +} + +async function resolveCssColor(page: Page, cssVariable: string): Promise { + return page.evaluate((variable) => { + const value = getComputedStyle(document.documentElement).getPropertyValue(variable).trim(); + const probe = document.createElement('div'); + + probe.style.backgroundColor = value; + document.body.appendChild(probe); + + const computedColor = getComputedStyle(probe).backgroundColor; + + probe.remove(); + + return computedColor; + }, cssVariable); +} diff --git a/src/@types/translations/en.json b/src/@types/translations/en.json index 4a2116567..eae6214a7 100644 --- a/src/@types/translations/en.json +++ b/src/@types/translations/en.json @@ -2559,6 +2559,8 @@ "addToColumnBottomTooltip": "Add a new card at the bottom", "renameColumn": "Rename", "hideColumn": "Hide", + "color": "Color", + "colorColumns": "Color columns", "newGroup": "New group", "deleteColumn": "Delete", "deleteCards": "This will delete all the cards in this group. Are you sure you want to continue?", @@ -2579,6 +2581,7 @@ "propertyName": "Property name", "menuName": "Board", "showUngrouped": "Show ungrouped items", + "colorColumns": "Color columns", "ungroupedButtonText": "Ungrouped", "ungroupedButtonTooltip": "Contains cards that don't belong in any group", "ungroupedItemsTitle": "Click to add to the board", diff --git a/src/application/database-yjs/__tests__/useGroup.test.tsx b/src/application/database-yjs/__tests__/useGroup.test.tsx index fc5e77e66..9ab7c71ba 100644 --- a/src/application/database-yjs/__tests__/useGroup.test.tsx +++ b/src/application/database-yjs/__tests__/useGroup.test.tsx @@ -14,11 +14,13 @@ function createDatabaseDoc({ groupId, groupColumns, viewId, + fieldType = FieldType.SingleSelect, }: { fieldId: string; groupId: string; groupColumns: unknown[]; viewId: string; + fieldType?: FieldType; }): YDoc { const doc = new Y.Doc() as unknown as YDoc; const sharedRoot = doc.getMap(YjsEditorKey.data_section); @@ -32,13 +34,13 @@ function createDatabaseDoc({ const columns = new Y.Array(); field.set(YjsDatabaseKey.id, fieldId); - field.set(YjsDatabaseKey.type, FieldType.SingleSelect); + field.set(YjsDatabaseKey.type, fieldType); fields.set(fieldId, field); columns.push(groupColumns); group.set(YjsDatabaseKey.id, groupId); group.set(YjsDatabaseKey.field_id, fieldId); - group.set(YjsDatabaseKey.type, FieldType.SingleSelect); + group.set(YjsDatabaseKey.type, fieldType); group.set(YjsDatabaseKey.groups, columns); groups.push([group]); diff --git a/src/application/database-yjs/dispatch.ts b/src/application/database-yjs/dispatch.ts index 07dc55fbb..e2195e8ba 100644 --- a/src/application/database-yjs/dispatch.ts +++ b/src/application/database-yjs/dispatch.ts @@ -228,6 +228,36 @@ function generateGroupByField(field: YDatabaseField) { return group; } +function getOrCreateBoardLayoutSetting(view: YDatabaseView) { + let layoutSettings = view.get(YjsDatabaseKey.layout_settings); + + if (!layoutSettings) { + layoutSettings = new Y.Map() as YDatabaseLayoutSettings; + view.set(YjsDatabaseKey.layout_settings, layoutSettings); + } + + let layoutSetting = layoutSettings.get('1'); + + if (!layoutSetting) { + layoutSetting = new Y.Map() as YDatabaseBoardLayoutSetting; + layoutSettings.set('1', layoutSetting); + } + + if (layoutSetting.get(YjsDatabaseKey.hide_ungrouped_column) === undefined) { + layoutSetting.set(YjsDatabaseKey.hide_ungrouped_column, false); + } + + if (layoutSetting.get(YjsDatabaseKey.collapse_hidden_groups) === undefined) { + layoutSetting.set(YjsDatabaseKey.collapse_hidden_groups, true); + } + + if (layoutSetting.get(YjsDatabaseKey.show_color_columns) === undefined) { + layoutSetting.set(YjsDatabaseKey.show_color_columns, true); + } + + return layoutSetting; +} + export function useGroupByFieldDispatch() { const view = useDatabaseView(); const database = useDatabase(); @@ -428,10 +458,11 @@ export function useToggleHiddenGroupColumnDispatch(groupId: string, fieldId: str throw new Error('Group columns not found'); } - const index = columns.toArray().findIndex((column) => column.id === columnId); - const column = columns.toArray().find((column) => column.id === columnId); + const columnsArray = columns.toArray(); + const index = columnsArray.findIndex((column) => column.id === columnId); + const column = index === -1 ? undefined : columnsArray[index]; - if (index === -1 || !column) { + if (!column) { throw new Error(`Column with id ${columnId} not found in group ${groupId}`); } @@ -470,19 +501,7 @@ export function useToggleCollapsedHiddenGroupColumnDispatch() { throw new Error(`Unable to toggle collapsed hidden group column`); } - // Get or create the layout settings for the view - let layoutSettings = view.get(YjsDatabaseKey.layout_settings); - - if (!layoutSettings) { - layoutSettings = new Y.Map() as YDatabaseLayoutSettings; - } - - let layoutSetting = layoutSettings.get('1'); - - if (!layoutSetting) { - layoutSetting = new Y.Map() as YDatabaseBoardLayoutSetting; - layoutSettings.set('1', layoutSetting); - } + const layoutSetting = getOrCreateBoardLayoutSetting(view); layoutSetting.set(YjsDatabaseKey.collapse_hidden_groups, collapsed); }, @@ -508,24 +527,38 @@ export function useToggleHideUnGrouped() { throw new Error(`Unable to toggle hide ungrouped column`); } - // Get or create the layout settings for the view - let layoutSettings = view.get(YjsDatabaseKey.layout_settings); + const layoutSetting = getOrCreateBoardLayoutSetting(view); - if (!layoutSettings) { - layoutSettings = new Y.Map() as YDatabaseLayoutSettings; - } + layoutSetting.set(YjsDatabaseKey.hide_ungrouped_column, hide); + }, + ], + 'toggleHideUnGrouped' + ); + }, + [sharedRoot, view] + ); +} - let layoutSetting = layoutSettings.get('1'); +export function useToggleShowColorColumns() { + const view = useDatabaseView(); + const sharedRoot = useSharedRoot(); - if (!layoutSetting) { - layoutSetting = new Y.Map() as YDatabaseBoardLayoutSetting; - layoutSettings.set('1', layoutSetting); + return useCallback( + (show: boolean) => { + executeOperations( + sharedRoot, + [ + () => { + if (!view) { + throw new Error(`Unable to toggle color columns`); } - layoutSetting.set(YjsDatabaseKey.hide_ungrouped_column, hide); + const layoutSetting = getOrCreateBoardLayoutSetting(view); + + layoutSetting.set(YjsDatabaseKey.show_color_columns, show); }, ], - 'toggleHideUnGrouped' + 'toggleShowColorColumns' ); }, [sharedRoot, view] @@ -1756,6 +1789,7 @@ function generateBoardLayoutSettings() { layoutSetting.set(YjsDatabaseKey.hide_ungrouped_column, false); layoutSetting.set(YjsDatabaseKey.collapse_hidden_groups, true); + layoutSetting.set(YjsDatabaseKey.show_color_columns, true); layoutSettings.set('1', layoutSetting); return layoutSettings; } diff --git a/src/application/database-yjs/selector.ts b/src/application/database-yjs/selector.ts index 7088acb93..d033b2a95 100644 --- a/src/application/database-yjs/selector.ts +++ b/src/application/database-yjs/selector.ts @@ -1032,27 +1032,34 @@ export function useGroup(groupId: string) { export function useBoardLayoutSettings() { const view = useDatabaseView(); - const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('1'); + const layoutSettings = view?.get(YjsDatabaseKey.layout_settings); + const layoutSetting = layoutSettings?.get('1'); const [isCollapsed, setIsCollapsed] = useState(true); const [hideUnGroup, setHideUnGroup] = useState(false); + const [showColorColumns, setShowColorColumns] = useState(true); const groups = view?.get(YjsDatabaseKey.groups); const [fieldId, setFieldId] = useState(null); useEffect(() => { - if (!layoutSetting) return; + if (!view) return; const observerEvent = () => { - setIsCollapsed(Boolean(layoutSetting?.get(YjsDatabaseKey.collapse_hidden_groups))); - setHideUnGroup(Boolean(layoutSetting?.get(YjsDatabaseKey.hide_ungrouped_column))); + const currentLayoutSetting = view.get(YjsDatabaseKey.layout_settings)?.get('1'); + + setIsCollapsed(currentLayoutSetting?.get(YjsDatabaseKey.collapse_hidden_groups) ?? true); + setHideUnGroup(currentLayoutSetting?.get(YjsDatabaseKey.hide_ungrouped_column) ?? false); + setShowColorColumns(currentLayoutSetting?.get(YjsDatabaseKey.show_color_columns) ?? true); }; observerEvent(); - layoutSetting.observe(observerEvent); + layoutSettings?.observeDeep(observerEvent); + view.observe(observerEvent); return () => { - layoutSetting.unobserve(observerEvent); + layoutSettings?.unobserveDeep(observerEvent); + view.unobserve(observerEvent); }; - }, [view, layoutSetting]); + }, [view, layoutSettings, layoutSetting]); useEffect(() => { const observerEvent = () => { @@ -1076,6 +1083,7 @@ export function useBoardLayoutSettings() { return { isCollapsed, hideUnGroup, + showColorColumns, fieldId, }; } diff --git a/src/application/types.ts b/src/application/types.ts index c18918551..cf95381b2 100644 --- a/src/application/types.ts +++ b/src/application/types.ts @@ -471,6 +471,7 @@ export enum YjsDatabaseKey { collapsed_group_ids = 'collapsed_group_ids', hide_ungrouped_column = 'hide_ungrouped_column', collapse_hidden_groups = 'collapse_hidden_groups', + show_color_columns = 'show_color_columns', first_day_of_week = 'first_day_of_week', show_week_numbers = 'show_week_numbers', show_weekends = 'show_weekends', @@ -764,7 +765,9 @@ export interface YDatabaseLayoutSettings extends Y.Map { } export interface YDatabaseBoardLayoutSetting extends Y.Map { - get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; + get( + key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups | YjsDatabaseKey.show_color_columns + ): boolean; } export interface YDatabaseCalendarLayoutSetting extends Y.Map { diff --git a/src/components/_shared/tag/Tag.tsx b/src/components/_shared/tag/Tag.tsx index 85f46284a..ef8738418 100644 --- a/src/components/_shared/tag/Tag.tsx +++ b/src/components/_shared/tag/Tag.tsx @@ -10,6 +10,11 @@ export interface TagProps { badge?: string; } +function toCssColor(color: string | undefined, fallback?: string) { + if (!color) return fallback; + return color.startsWith('--') ? `var(${color})` : color; +} + export const Tag: FC = ({ bgColor, textColor, label, badge }) => { const className = useMemo(() => { return cn( @@ -22,15 +27,15 @@ export const Tag: FC = ({ bgColor, textColor, label, badge }) => { return (
{badge && ( diff --git a/src/components/database/components/board/card/CardPrimitive.tsx b/src/components/database/components/board/card/CardPrimitive.tsx index a8e66275e..83257cc30 100644 --- a/src/components/database/components/board/card/CardPrimitive.tsx +++ b/src/components/database/components/board/card/CardPrimitive.tsx @@ -162,9 +162,9 @@ export const CardPrimitive = forwardRef( ref={ref} data-card-id={dataCardId} className={cn( - 'board-card relative flex flex-col overflow-hidden rounded-[6px] text-xs shadow-card', + 'board-card relative flex flex-col overflow-hidden rounded-[6px] text-xs', navigateToRow && 'cursor-pointer hover:bg-fill-content-hover', - selected && 'ring-1 ring-border-theme-thick', + selected && 'board-card-selected', className )} > diff --git a/src/components/database/components/board/card/NewCard.tsx b/src/components/database/components/board/card/NewCard.tsx index 52b3405d6..5e875c543 100644 --- a/src/components/database/components/board/card/NewCard.tsx +++ b/src/components/database/components/board/card/NewCard.tsx @@ -6,11 +6,12 @@ import { useNewRowDispatch } from '@/application/database-yjs/dispatch'; import { ReactComponent as PlusIcon } from '@/assets/icons/plus.svg'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { createHotkey, HOT_KEY_NAME } from '@/utils/hotkeys'; const BOUNDARY_GAP = 100; +const NEW_CARD_HEIGHT = 44; +const NEW_CARD_CONTROL_HEIGHT = 36; function NewCard({ beforeId, @@ -31,24 +32,24 @@ function NewCard({ const [value, setValue] = useState(''); const onNewCard = useNewRowDispatch(); const inputRef = useRef(null); - const ref = useRef(null); - const [container, setContainer] = useState(null); + const containerRef = useRef(null); const scrollToBottom = useCallback(() => { - if (!container) return; + if (!containerRef.current) return; - const scrollElement = container.closest('.appflowy-scroll-container') as HTMLDivElement; - const rect = container.getBoundingClientRect(); + const scrollElement = containerRef.current.closest('.appflowy-scroll-container') as HTMLDivElement; + const rect = containerRef.current.getBoundingClientRect(); const scrollY = rect.bottom + BOUNDARY_GAP - window.innerHeight; if (scrollY <= 0) return; + if (!scrollElement) return; scrollElement.scrollBy({ top: scrollY, behavior: 'smooth', }); - }, [container]); + }, []); const handleSubmit = useCallback( (inputValue: string) => { @@ -76,68 +77,73 @@ function NewCard({ ); useLayoutEffect(() => { - if (!ref.current) return; - - const el = ref.current.parentElement as HTMLDivElement | null; + if (!isCreating || !inputRef.current) return; - if (!el) return; + inputRef.current.focus(); + inputRef.current.setSelectionRange(0, inputRef.current.value.length); + }, [isCreating]); - setContainer(el); - }, []); + const handleClose = useCallback(() => { + setIsCreating(false); + }, [setIsCreating]); return ( - - - - - - e.preventDefault()} - onKeyDown={(e) => { - if (createHotkey(HOT_KEY_NAME.ENTER)(e.nativeEvent)) { - handleSubmit(value); - } - }} - > +
+ {isCreating ? ( { - if (!input) return; - if (!inputRef.current) { - setTimeout(() => { - input.setSelectionRange(0, input.value.length); - }, 100); - - inputRef.current = input; - } - }} + ref={inputRef} value={value} onChange={(e) => { setValue(e.target.value); }} - className={'!h-9 w-full !rounded-300 text-text-primary'} + onBlur={handleClose} + onKeyDown={(e) => { + if (createHotkey(HOT_KEY_NAME.ENTER)(e.nativeEvent)) { + e.preventDefault(); + e.stopPropagation(); + handleSubmit(value); + return; + } + + if (createHotkey(HOT_KEY_NAME.ESCAPE)(e.nativeEvent)) { + e.preventDefault(); + e.stopPropagation(); + handleClose(); + } + }} + className={'w-full !rounded-[6px] !bg-transparent px-3 text-text-primary shadow-none'} + style={{ + height: NEW_CARD_CONTROL_HEIGHT, + }} /> - - + ) : ( + + + + + {t('board.column.addToColumnBottomTooltip')} + + )} +
); } diff --git a/src/components/database/components/board/column/CardList.tsx b/src/components/database/components/board/column/CardList.tsx index 9cb0a3bd2..acfc8c881 100644 --- a/src/components/database/components/board/column/CardList.tsx +++ b/src/components/database/components/board/column/CardList.tsx @@ -1,7 +1,6 @@ import { useVirtualizer } from '@tanstack/react-virtual'; import { memo, useCallback, useLayoutEffect, useMemo, useRef } from 'react'; -import { PADDING_END } from '@/application/database-yjs'; import { useBoardActions, useBoardSelection } from '@/components/database/board/BoardProvider'; import { Card } from '@/components/database/components/board/card'; import { cn } from '@/lib/utils'; @@ -17,9 +16,10 @@ export interface RenderCard { } const CARD_LIST_MAX_HEIGHT = 2000; +const CARD_LIST_ESTIMATED_ITEM_HEIGHT = 50; const CARD_LIST_STYLE = { - maxHeight: CARD_LIST_MAX_HEIGHT, + maxHeight: `min(100%, ${CARD_LIST_MAX_HEIGHT}px)`, overflowY: 'auto', } as const; @@ -72,21 +72,22 @@ function CardList({ scrollMargin: 0, // Always 0 for Board - items are positioned relative to column top overscan: 5, getScrollElement, - estimateSize: () => 36, + estimateSize: () => CARD_LIST_ESTIMATED_ITEM_HEIGHT, paddingStart: 0, - paddingEnd: PADDING_END, + paddingEnd: 0, getItemKey: (index) => data[index].id || String(index), }); const virtualItems = virtualizer.getVirtualItems(); + const totalSize = virtualizer.getTotalSize(); const viewportHeight = Math.min( parentRef.current?.clientHeight || CARD_LIST_MAX_HEIGHT, CARD_LIST_MAX_HEIGHT ); const scrollTop = parentRef.current?.scrollTop ?? 0; - const renderStart = Math.max(0, scrollTop - 5 * 36); - const renderEnd = scrollTop + viewportHeight + 5 * 36; - const maxRenderedItems = Math.ceil(viewportHeight / 36) + 12; + const renderStart = Math.max(0, scrollTop - 5 * CARD_LIST_ESTIMATED_ITEM_HEIGHT); + const renderEnd = scrollTop + viewportHeight + 5 * CARD_LIST_ESTIMATED_ITEM_HEIGHT; + const maxRenderedItems = Math.ceil(viewportHeight / CARD_LIST_ESTIMATED_ITEM_HEIGHT) + 12; const items = virtualItems.length > maxRenderedItems ? virtualItems .filter((item) => item.end >= renderStart && item.start <= renderEnd) @@ -96,12 +97,15 @@ function CardList({ return (
void; groupId: string; + showColorColumns: boolean; } function areRowsEqual(prevRows: Row[], nextRows: Row[]) { @@ -38,14 +40,20 @@ function areColumnPropsEqual(prev: ColumnProps, next: ColumnProps) { prev.id === next.id && prev.fieldId === next.fieldId && prev.groupId === next.groupId && + prev.showColorColumns === next.showColorColumns && prev.addCardBefore === next.addCardBefore && areRowsEqual(prev.rows, next.rows) ); } export const Column = memo( - ({ id, rows, fieldId, addCardBefore, groupId }: ColumnProps) => { + ({ id, rows, fieldId, addCardBefore, groupId, showColorColumns }: ColumnProps) => { const readOnly = useReadOnly(); + const { style: colorStyle } = useBoardColumnColor({ + id, + fieldId, + showColorColumns, + }); const data: RenderCard[] = useMemo(() => { const cards = rows.map((row) => ({ @@ -75,14 +83,18 @@ export const Column = memo( return ( -
+
- +
{state.type === StateType.IS_COLUMN_OVER && state.closestEdge && ( diff --git a/src/components/database/components/board/column/ColumnHeader.tsx b/src/components/database/components/board/column/ColumnHeader.tsx index cd883d217..12ba4434e 100644 --- a/src/components/database/components/board/column/ColumnHeader.tsx +++ b/src/components/database/components/board/column/ColumnHeader.tsx @@ -1,16 +1,17 @@ - import { Row, useReadOnly } from '@/application/database-yjs'; +import { useBoardColumnColor } from '@/components/database/components/board/column/boardColumnColor'; import ColumnHeaderPrimitive from '@/components/database/components/board/column/ColumnHeaderPrimitive'; import { useColumnHeaderDrag, StateType } from '@/components/database/components/board/column/useColumnHeaderDrag'; import { DropColumnIndicator } from '@/components/database/components/board/drag-and-drop/DropColumnIndicator'; -function ColumnHeader ({ +function ColumnHeader({ id, fieldId, rowCount, addCardBefore, getCards, groupId, + showColorColumns, }: { id: string; fieldId: string; @@ -18,14 +19,15 @@ function ColumnHeader ({ addCardBefore: (id: string) => void; getCards: (id: string) => Row[]; groupId: string; + showColorColumns: boolean; }) { - const { - columnRef, - headerRef, - state, - isDragging, - } = useColumnHeaderDrag(id); + const { columnRef, headerRef, state, isDragging } = useColumnHeaderDrag(id); const readOnly = useReadOnly(); + const { style: colorStyle } = useBoardColumnColor({ + id, + fieldId, + showColorColumns, + }); return (
- {state.type === StateType.IS_COLUMN_OVER && state.closestEdge && ( - - )} + {state.type === StateType.IS_COLUMN_OVER && state.closestEdge && }
); } -export default ColumnHeader; \ No newline at end of file +export default ColumnHeader; diff --git a/src/components/database/components/board/column/ColumnHeaderPrimitive.tsx b/src/components/database/components/board/column/ColumnHeaderPrimitive.tsx index fe543829c..dc4e8c1c8 100644 --- a/src/components/database/components/board/column/ColumnHeaderPrimitive.tsx +++ b/src/components/database/components/board/column/ColumnHeaderPrimitive.tsx @@ -15,10 +15,10 @@ function ColumnHeaderPrimitive( id, fieldId, className, - rowCount, addCardBefore, getCards, groupId, + showColorColumns, ...props }: { id: string; @@ -27,10 +27,11 @@ function ColumnHeaderPrimitive( getCards: (id: string) => Row[]; addCardBefore: (id: string) => void; groupId: string; + showColorColumns?: boolean; } & React.HTMLAttributes, ref: React.Ref ) { - const { header, renameEnabled, deleteEnabled, hideEnabled } = useRenderColumn(id, fieldId); + const { header, renameEnabled, deleteEnabled, hideEnabled } = useRenderColumn(id, fieldId, Boolean(showColorColumns)); const { t } = useTranslation(); const readOnly = useReadOnly(); @@ -45,7 +46,6 @@ function ColumnHeaderPrimitive( >
{header}
- {rowCount}
{!readOnly && (
@@ -57,6 +57,7 @@ function ColumnHeaderPrimitive( deleteEnabled={deleteEnabled} hideEnabled={hideEnabled} getCards={getCards} + showColorColumns={Boolean(showColorColumns)} > + ))} + + )}
diff --git a/src/components/database/components/board/column/boardColumnColor.ts b/src/components/database/components/board/column/boardColumnColor.ts new file mode 100644 index 000000000..afd291cf3 --- /dev/null +++ b/src/components/database/components/board/column/boardColumnColor.ts @@ -0,0 +1,233 @@ +import { useMemo } from 'react'; + +import { + FieldType, + parseSelectOptionTypeOptions, + SelectOption, + SelectOptionColor, + useFieldSelector, +} from '@/application/database-yjs'; +import { YjsDatabaseKey } from '@/application/types'; + +export interface BoardColumnColorStyle { + backgroundColor: string; + highlightColor: string; + labelColor: string; + paletteColor: string; + textColor: string; +} + +export const BOARD_COLUMN_COLOR_OPTIONS = [ + SelectOptionColor.OptionColor1, + SelectOptionColor.OptionColor2, + SelectOptionColor.OptionColor3, + SelectOptionColor.OptionColor4, + SelectOptionColor.OptionColor5, + SelectOptionColor.OptionColor6, + SelectOptionColor.OptionColor7, + SelectOptionColor.OptionColor8, + SelectOptionColor.OptionColor9, + SelectOptionColor.OptionColor10, +]; + +export type BoardColumnColorLabelKey = + | 'colors.mauve' + | 'colors.lilac' + | 'colors.camellia' + | 'colors.papaya' + | 'colors.mango' + | 'colors.olive' + | 'colors.grass' + | 'colors.jade' + | 'colors.azure' + | 'colors.iron' + | 'colors.mauveEmphasized' + | 'colors.lavenderEmphasized' + | 'colors.camelliaEmphasized' + | 'colors.papayaEmphasized' + | 'colors.mangoEmphasized' + | 'colors.oliveEmphasized' + | 'colors.grassEmphasized' + | 'colors.jadeEmphasized' + | 'colors.azureEmphasized' + | 'colors.ironEmphasized'; + +const BOARD_COLUMN_COLOR_LABEL_KEYS: Record = { + [SelectOptionColor.OptionColor1]: 'colors.mauve', + [SelectOptionColor.OptionColor2]: 'colors.lilac', + [SelectOptionColor.OptionColor3]: 'colors.camellia', + [SelectOptionColor.OptionColor4]: 'colors.papaya', + [SelectOptionColor.OptionColor5]: 'colors.mango', + [SelectOptionColor.OptionColor6]: 'colors.olive', + [SelectOptionColor.OptionColor7]: 'colors.grass', + [SelectOptionColor.OptionColor8]: 'colors.jade', + [SelectOptionColor.OptionColor9]: 'colors.azure', + [SelectOptionColor.OptionColor10]: 'colors.iron', + [SelectOptionColor.OptionColor11]: 'colors.mauveEmphasized', + [SelectOptionColor.OptionColor12]: 'colors.lavenderEmphasized', + [SelectOptionColor.OptionColor13]: 'colors.camelliaEmphasized', + [SelectOptionColor.OptionColor14]: 'colors.papayaEmphasized', + [SelectOptionColor.OptionColor15]: 'colors.mangoEmphasized', + [SelectOptionColor.OptionColor16]: 'colors.oliveEmphasized', + [SelectOptionColor.OptionColor17]: 'colors.grassEmphasized', + [SelectOptionColor.OptionColor18]: 'colors.jadeEmphasized', + [SelectOptionColor.OptionColor19]: 'colors.azureEmphasized', + [SelectOptionColor.OptionColor20]: 'colors.ironEmphasized', +}; + +const SELECT_OPTION_TO_PALETTE_COLOR: Record = { + [SelectOptionColor.OptionColor1]: '--palette-bg-color-14', + [SelectOptionColor.OptionColor2]: '--palette-bg-color-16', + [SelectOptionColor.OptionColor3]: '--palette-bg-color-18', + [SelectOptionColor.OptionColor4]: '--palette-bg-color-2', + [SelectOptionColor.OptionColor5]: '--palette-bg-color-4', + [SelectOptionColor.OptionColor6]: '--palette-bg-color-6', + [SelectOptionColor.OptionColor7]: '--palette-bg-color-8', + [SelectOptionColor.OptionColor8]: '--palette-bg-color-10', + [SelectOptionColor.OptionColor9]: '--palette-bg-color-12', + [SelectOptionColor.OptionColor10]: '--palette-bg-color-20', + [SelectOptionColor.OptionColor11]: '--palette-bg-color-14', + [SelectOptionColor.OptionColor12]: '--palette-bg-color-16', + [SelectOptionColor.OptionColor13]: '--palette-bg-color-18', + [SelectOptionColor.OptionColor14]: '--palette-bg-color-2', + [SelectOptionColor.OptionColor15]: '--palette-bg-color-4', + [SelectOptionColor.OptionColor16]: '--palette-bg-color-6', + [SelectOptionColor.OptionColor17]: '--palette-bg-color-8', + [SelectOptionColor.OptionColor18]: '--palette-bg-color-10', + [SelectOptionColor.OptionColor19]: '--palette-bg-color-12', + [SelectOptionColor.OptionColor20]: '--palette-bg-color-20', +}; + +const SELECT_OPTION_TO_BACKGROUND_COLOR: Record = { + [SelectOptionColor.OptionColor1]: '--block-bg-color-14', + [SelectOptionColor.OptionColor2]: '--block-bg-color-16', + [SelectOptionColor.OptionColor3]: '--block-bg-color-18', + [SelectOptionColor.OptionColor4]: '--block-bg-color-2', + [SelectOptionColor.OptionColor5]: '--block-bg-color-4', + [SelectOptionColor.OptionColor6]: '--block-bg-color-6', + [SelectOptionColor.OptionColor7]: '--block-bg-color-8', + [SelectOptionColor.OptionColor8]: '--block-bg-color-10', + [SelectOptionColor.OptionColor9]: '--block-bg-color-12', + [SelectOptionColor.OptionColor10]: '--block-bg-color-20', + [SelectOptionColor.OptionColor11]: '--block-bg-color-14', + [SelectOptionColor.OptionColor12]: '--block-bg-color-16', + [SelectOptionColor.OptionColor13]: '--block-bg-color-18', + [SelectOptionColor.OptionColor14]: '--block-bg-color-2', + [SelectOptionColor.OptionColor15]: '--block-bg-color-4', + [SelectOptionColor.OptionColor16]: '--block-bg-color-6', + [SelectOptionColor.OptionColor17]: '--block-bg-color-8', + [SelectOptionColor.OptionColor18]: '--block-bg-color-10', + [SelectOptionColor.OptionColor19]: '--block-bg-color-12', + [SelectOptionColor.OptionColor20]: '--block-bg-color-20', +}; + +const SELECT_OPTION_TO_LABEL_COLOR: Record = { + [SelectOptionColor.OptionColor1]: '--block-border-color-14', + [SelectOptionColor.OptionColor2]: '--block-border-color-16', + [SelectOptionColor.OptionColor3]: '--block-border-color-18', + [SelectOptionColor.OptionColor4]: '--block-border-color-2', + [SelectOptionColor.OptionColor5]: '--block-border-color-4', + [SelectOptionColor.OptionColor6]: '--block-border-color-6', + [SelectOptionColor.OptionColor7]: '--block-border-color-8', + [SelectOptionColor.OptionColor8]: '--block-border-color-10', + [SelectOptionColor.OptionColor9]: '--block-border-color-12', + [SelectOptionColor.OptionColor10]: '--block-border-color-20', + [SelectOptionColor.OptionColor11]: '--block-border-color-14', + [SelectOptionColor.OptionColor12]: '--block-border-color-16', + [SelectOptionColor.OptionColor13]: '--block-border-color-18', + [SelectOptionColor.OptionColor14]: '--block-border-color-2', + [SelectOptionColor.OptionColor15]: '--block-border-color-4', + [SelectOptionColor.OptionColor16]: '--block-border-color-6', + [SelectOptionColor.OptionColor17]: '--block-border-color-8', + [SelectOptionColor.OptionColor18]: '--block-border-color-10', + [SelectOptionColor.OptionColor19]: '--block-border-color-12', + [SelectOptionColor.OptionColor20]: '--block-border-color-20', +}; + +const SELECT_OPTION_TO_TEXT_COLOR: Record = { + [SelectOptionColor.OptionColor1]: '--block-text-color-14', + [SelectOptionColor.OptionColor2]: '--block-text-color-16', + [SelectOptionColor.OptionColor3]: '--block-text-color-18', + [SelectOptionColor.OptionColor4]: '--block-text-color-2', + [SelectOptionColor.OptionColor5]: '--block-text-color-4', + [SelectOptionColor.OptionColor6]: '--block-text-color-6', + [SelectOptionColor.OptionColor7]: '--block-text-color-8', + [SelectOptionColor.OptionColor8]: '--block-text-color-10', + [SelectOptionColor.OptionColor9]: '--block-text-color-12', + [SelectOptionColor.OptionColor10]: '--block-text-color-20', + [SelectOptionColor.OptionColor11]: '--block-text-color-14', + [SelectOptionColor.OptionColor12]: '--block-text-color-16', + [SelectOptionColor.OptionColor13]: '--block-text-color-18', + [SelectOptionColor.OptionColor14]: '--block-text-color-2', + [SelectOptionColor.OptionColor15]: '--block-text-color-4', + [SelectOptionColor.OptionColor16]: '--block-text-color-6', + [SelectOptionColor.OptionColor17]: '--block-text-color-8', + [SelectOptionColor.OptionColor18]: '--block-text-color-10', + [SelectOptionColor.OptionColor19]: '--block-text-color-12', + [SelectOptionColor.OptionColor20]: '--block-text-color-20', +}; + +function cssVar(token?: string) { + return token ? `var(${token})` : undefined; +} + +export function getBoardColumnColorStyle(color: SelectOptionColor | undefined): BoardColumnColorStyle | undefined { + if (!color) return undefined; + + const backgroundColor = cssVar(SELECT_OPTION_TO_BACKGROUND_COLOR[color]); + const labelColor = cssVar(SELECT_OPTION_TO_LABEL_COLOR[color]); + const paletteColor = cssVar(SELECT_OPTION_TO_PALETTE_COLOR[color]); + const textColor = cssVar(SELECT_OPTION_TO_TEXT_COLOR[color]); + + if (!backgroundColor || !labelColor || !paletteColor || !textColor) return undefined; + + return { + backgroundColor, + highlightColor: backgroundColor, + labelColor, + paletteColor, + textColor, + }; +} + +export function getBoardColumnColorLabelKey(color: SelectOptionColor) { + return BOARD_COLUMN_COLOR_LABEL_KEYS[color]; +} + +export function useBoardColumnColor({ + id, + fieldId, + showColorColumns, +}: { + id: string; + fieldId: string; + showColorColumns: boolean; +}): { option: SelectOption | undefined; style: BoardColumnColorStyle | undefined } { + const { field, clock } = useFieldSelector(fieldId); + + return useMemo(() => { + if (!showColorColumns || id === fieldId || !field) { + return { + option: undefined, + style: undefined, + }; + } + + const fieldType = Number(field.get(YjsDatabaseKey.type)) as FieldType; + + if (![FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { + return { + option: undefined, + style: undefined, + }; + } + + const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option?.id === id); + + return { + option, + style: getBoardColumnColorStyle(option?.color), + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [clock, field, fieldId, id, showColorColumns]); +} diff --git a/src/components/database/components/board/column/columnName.ts b/src/components/database/components/board/column/columnName.ts new file mode 100644 index 000000000..3a860c0a1 --- /dev/null +++ b/src/components/database/components/board/column/columnName.ts @@ -0,0 +1,43 @@ +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; + +import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs'; +import { getChecked } from '@/application/database-yjs/fields/checkbox/utils'; +import { YDatabaseField, YjsDatabaseKey } from '@/application/types'; + +export function getBoardColumnName({ + id, + fieldId, + field, + fieldType, + t, +}: { + id: string; + fieldId: string; + field?: YDatabaseField; + fieldType: FieldType; + t: TFunction; +}) { + if (!field) return id; + + if (fieldType === FieldType.Checkbox) { + return getChecked(id) ? t('button.yes') : t('button.no'); + } + + if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { + const fieldName = field.get(YjsDatabaseKey.name) || ''; + const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option?.id === id); + + return fieldId === id ? `${t('button.no')} ${fieldName}` : option?.name || id; + } + + return id; +} + +export function useBoardColumnName(id: string, fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const { t } = useTranslation(); + + return getBoardColumnName({ id, fieldId, field, fieldType, t }); +} diff --git a/src/components/database/components/board/column/useRenderColumn.tsx b/src/components/database/components/board/column/useRenderColumn.tsx index 76bd219a1..b09b357d5 100644 --- a/src/components/database/components/board/column/useRenderColumn.tsx +++ b/src/components/database/components/board/column/useRenderColumn.tsx @@ -8,13 +8,15 @@ import { YjsDatabaseKey } from '@/application/types'; import { ReactComponent as CheckboxCheckSvg } from '@/assets/icons/check_filled.svg'; import { ReactComponent as CheckboxUncheckSvg } from '@/assets/icons/uncheck.svg'; import { Tag } from '@/components/_shared/tag'; +import { getBoardColumnColorStyle } from '@/components/database/components/board/column/boardColumnColor'; +import { getBoardColumnName } from '@/components/database/components/board/column/columnName'; import { SelectOptionColorMap, SelectOptionFgColorMap } from '@/components/database/components/cell/cell.const'; -export function useRenderColumn(id: string, fieldId: string) { +export function useRenderColumn(id: string, fieldId: string, showColorColumns = false) { const { field, clock } = useFieldSelector(fieldId); const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; - const fieldName = field?.get(YjsDatabaseKey.name) || ''; const { t } = useTranslation(); + const label = getBoardColumnName({ id, fieldId, field, fieldType, t }); const header = useMemo(() => { if (!field) return null; if (fieldType === FieldType.Checkbox) @@ -23,29 +25,30 @@ export function useRenderColumn(id: string, fieldId: string) { {getChecked(id) ? ( <> - {t('button.checked')} + {label} ) : ( <> {' '} - {t('button.unchecked')} + {label} )}
); if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) { const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option?.id === id); - const isFieldId = fieldId === id; - const label = isFieldId ? `${t('button.no')} ${fieldName}` : option?.name || ''; + const colorStyle = showColorColumns ? getBoardColumnColorStyle(option?.color) : undefined; return ( @@ -54,7 +57,7 @@ export function useRenderColumn(id: string, fieldId: string) { return null; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [field, clock, fieldType, id, fieldName, t]); + }, [field, clock, fieldType, id, label, showColorColumns]); const renameEnabled = useMemo(() => { return [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType); diff --git a/src/components/database/components/board/group/Columns.tsx b/src/components/database/components/board/group/Columns.tsx index 4072f61ef..a3fc45c1d 100644 --- a/src/components/database/components/board/group/Columns.tsx +++ b/src/components/database/components/board/group/Columns.tsx @@ -1,6 +1,13 @@ import { forwardRef, useCallback, useMemo } from 'react'; -import { FieldType, GroupColumn, Row, useFieldType, useReadOnly } from '@/application/database-yjs'; +import { + FieldType, + GroupColumn, + Row, + useBoardLayoutSettings, + useFieldType, + useReadOnly, +} from '@/application/database-yjs'; import { Column } from '@/components/database/components/board/column'; import HiddenGroupColumn from '@/components/database/components/board/column/HiddenGroupColumn'; import AddGroupColumn from '@/components/database/components/board/group/AddGroupColumn'; @@ -45,13 +52,21 @@ const Columns = forwardRef< }, [columns, groupResult]); const readOnly = useReadOnly(); + const { showColorColumns } = useBoardLayoutSettings(); return (
{!readOnly && } {columnsWithRows.map((data) => ( - + ))} {isSelectField && !readOnly && }
diff --git a/src/components/database/components/board/group/GroupHeader.tsx b/src/components/database/components/board/group/GroupHeader.tsx index e351717bf..4f909e29a 100644 --- a/src/components/database/components/board/group/GroupHeader.tsx +++ b/src/components/database/components/board/group/GroupHeader.tsx @@ -1,41 +1,39 @@ import { forwardRef, useCallback, useMemo } from 'react'; -import { FieldType, GroupColumn, Row, useFieldType, useReadOnly } from '@/application/database-yjs'; +import { + FieldType, + GroupColumn, + Row, + useBoardLayoutSettings, + useFieldType, + useReadOnly, +} from '@/application/database-yjs'; import ColumnHeader from '@/components/database/components/board/column/ColumnHeader'; import HiddenGroupColumnHeader from '@/components/database/components/board/column/HiddenGroupColumnHeader'; import AddGroupColumn from '@/components/database/components/board/group/AddGroupColumn'; -const GroupHeader = forwardRef; - addCardBefore: (id: string) => void; - groupId: string; -}>(({ - columns, - fieldId, - groupResult, - addCardBefore, - groupId, -}, ref) => { +const GroupHeader = forwardRef< + HTMLDivElement, + { + columns: GroupColumn[]; + fieldId: string; + + groupResult: Map; + addCardBefore: (id: string) => void; + groupId: string; + } +>(({ columns, fieldId, groupResult, addCardBefore, groupId }, ref) => { const readOnly = useReadOnly(); + const { showColorColumns } = useBoardLayoutSettings(); const getCards = useCallback((id: string) => groupResult.get(id) || [], [groupResult]); const fieldType = useFieldType(fieldId); const isSelectField = useMemo(() => { - return [ - FieldType.SingleSelect, - FieldType.MultiSelect, - ].includes(fieldType); + return [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType); }, [fieldType]); return ( -
- +
{!readOnly && } {columns.map((data) => ( @@ -47,14 +45,12 @@ const GroupHeader = forwardRef ))} - {isSelectField && !readOnly && } + {isSelectField && !readOnly && }
); }); -export default GroupHeader; \ No newline at end of file +export default GroupHeader; diff --git a/src/components/database/components/settings/BoardSettingGroup.tsx b/src/components/database/components/settings/BoardSettingGroup.tsx index 82aeddfd5..9cb1eadb2 100644 --- a/src/components/database/components/settings/BoardSettingGroup.tsx +++ b/src/components/database/components/settings/BoardSettingGroup.tsx @@ -10,23 +10,23 @@ import { DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuItem, DropdownMenuLabel, DropdownMenuItemTick, DropdownMenuSeparator, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuItemTick, + DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; import { Switch } from '@/components/ui/switch'; -function BoardSettingGroup () { +function BoardSettingGroup() { const { t } = useTranslation(); - const { - hideUnGroup, - fieldId, - } = useBoardLayoutSettings(); + const { hideUnGroup, fieldId } = useBoardLayoutSettings(); const fieldType = useFieldType(fieldId || ''); const toggle = useToggleHideUnGrouped(); const groupBy = useGroupByFieldDispatch(); const { properties: allProperties } = usePropertiesSelector(true); const properties = useMemo(() => { - return allProperties.filter(property => { + return allProperties.filter((property) => { const type = property.type; return [ @@ -47,9 +47,7 @@ function BoardSettingGroup () { {t('grid.settings.group')} - + {fieldType !== FieldType.Checkbox && ( <> {t('board.showUngrouped')} - - + )} {t('board.groupBy')} - {properties.map(property => ( + {properties.map((property) => ( {fieldId === property.id && } - ))} - ); } -export default BoardSettingGroup; \ No newline at end of file +export default BoardSettingGroup; diff --git a/src/components/database/components/settings/Layout.tsx b/src/components/database/components/settings/Layout.tsx index 069fc7feb..3af8b8254 100644 --- a/src/components/database/components/settings/Layout.tsx +++ b/src/components/database/components/settings/Layout.tsx @@ -1,24 +1,30 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDatabaseViewId } from '@/application/database-yjs'; -import { useUpdateDatabaseLayout } from '@/application/database-yjs/dispatch'; +import { useBoardLayoutSettings, useDatabaseViewId } from '@/application/database-yjs'; +import { useToggleShowColorColumns, useUpdateDatabaseLayout } from '@/application/database-yjs/dispatch'; import { DatabaseViewLayout } from '@/application/types'; import { ReactComponent as LayoutIcon } from '@/assets/icons/layout.svg'; +import { ReactComponent as PaletteIcon } from '@/assets/icons/palette.svg'; import { DropdownMenuItem, DropdownMenuItemTick, DropdownMenuPortal, + DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from '@/components/ui/dropdown-menu'; +import { Switch } from '@/components/ui/switch'; function Layout({ currentLayout }: { currentLayout: DatabaseViewLayout }) { const { t } = useTranslation(); const viewId = useDatabaseViewId(); const updateLayout = useUpdateDatabaseLayout(viewId); + const toggleColorColumns = useToggleShowColorColumns(); + const { showColorColumns } = useBoardLayoutSettings(); + const isBoardLayout = currentLayout === DatabaseViewLayout.Board; const options = useMemo( () => [ { @@ -61,6 +67,22 @@ function Layout({ currentLayout }: { currentLayout: DatabaseViewLayout }) { {currentLayout === option.value && } ))} + {isBoardLayout && ( + <> + + { + e.preventDefault(); + toggleColorColumns(!showColorColumns); + }} + > + + {t('board.column.colorColumns')} + + + + )} diff --git a/src/styles/app.scss b/src/styles/app.scss index d4a978b2a..29c96db69 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -177,9 +177,21 @@ body { } .board-card { - @apply border-border-primary border-[1px]; border-radius: 6px; background: var(--background-primary); + border: 1px solid rgb(31 35 41 / 12%); + box-shadow: + 0 0 4px rgb(31 35 41 / 2%), + 0 0 4px -2px rgb(31 35 41 / 2%); +} + +:root[data-dark-mode='true'] .board-card { + border-color: #59647a; +} + +.board-card.board-card-selected, +:root[data-dark-mode='true'] .board-card.board-card-selected { + border-color: var(--board-card-highlight-color, var(--border-theme-thick)); } .no-scrollbar::-webkit-scrollbar {