mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support preview the calendar view on web (#5394)
This commit is contained in:
parent
68c4e19f91
commit
acae34836e
@ -34,7 +34,7 @@ dependencies:
|
||||
version: 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/x-date-pickers-pro':
|
||||
specifier: ^6.18.2
|
||||
version: 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@reduxjs/toolkit':
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0(react-redux@8.0.5)(react@18.2.0)
|
||||
@ -1763,8 +1763,8 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/system@5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA==}
|
||||
/@mui/system@5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.5.0
|
||||
@ -1870,7 +1870,7 @@ packages:
|
||||
react-is: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
/@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
@ -1912,9 +1912,9 @@ packages:
|
||||
'@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0)
|
||||
'@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/material': 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0)
|
||||
'@mui/system': 5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0)
|
||||
'@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0)
|
||||
'@mui/x-date-pickers': 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/x-date-pickers': 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/x-license-pro': 6.10.2(@types/react@18.2.66)(react@18.2.0)
|
||||
clsx: 2.1.0
|
||||
dayjs: 1.11.9
|
||||
@ -1926,7 +1926,7 @@ packages:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@mui/x-date-pickers@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
/@mui/x-date-pickers@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
@ -1968,7 +1968,7 @@ packages:
|
||||
'@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0)
|
||||
'@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/material': 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0)
|
||||
'@mui/system': 5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0)
|
||||
'@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0)
|
||||
'@types/react-transition-group': 4.4.10
|
||||
clsx: 2.1.0
|
||||
|
@ -244,6 +244,10 @@ export enum YjsDatabaseKey {
|
||||
visible = 'visible',
|
||||
hide_ungrouped_column = 'hide_ungrouped_column',
|
||||
collapse_hidden_groups = 'collapse_hidden_groups',
|
||||
first_day_of_week = 'first_day_of_week',
|
||||
show_week_numbers = 'show_week_numbers',
|
||||
show_weekends = 'show_weekends',
|
||||
layout_ty = 'layout_ty',
|
||||
}
|
||||
|
||||
export interface YDoc extends Y.Doc {
|
||||
@ -434,23 +438,30 @@ export type YDatabaseFilters = Y.Array<YDatabaseFilter>;
|
||||
|
||||
export type YDatabaseSorts = Y.Array<YDatabaseSort>;
|
||||
|
||||
export type YDatabaseLayoutSettings = Y.Map<YDatabaseLayoutSetting>;
|
||||
|
||||
export type YDatabaseCalculations = Y.Array<YDatabaseCalculation>;
|
||||
|
||||
export type SortId = string;
|
||||
|
||||
export type GroupId = string;
|
||||
|
||||
export interface YDatabaseLayoutSetting extends Y.Map<unknown> {
|
||||
export interface YDatabaseLayoutSettings extends Y.Map<unknown> {
|
||||
// DatabaseViewLayout.Board
|
||||
get(key: '2'): YDatabaseBoardLayoutSetting;
|
||||
get(key: '1'): YDatabaseBoardLayoutSetting;
|
||||
|
||||
// DatabaseViewLayout.Calendar
|
||||
get(key: '2'): YDatabaseCalendarLayoutSetting;
|
||||
}
|
||||
|
||||
export interface YDatabaseBoardLayoutSetting extends Y.Map<unknown> {
|
||||
get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean;
|
||||
}
|
||||
|
||||
export interface YDatabaseCalendarLayoutSetting extends Y.Map<unknown> {
|
||||
get(key: YjsDatabaseKey.first_day_of_week | YjsDatabaseKey.field_id | YjsDatabaseKey.layout_ty): string;
|
||||
|
||||
get(key: YjsDatabaseKey.show_week_numbers | YjsDatabaseKey.show_weekends): boolean;
|
||||
}
|
||||
|
||||
export interface YDatabaseGroup extends Y.Map<unknown> {
|
||||
get(key: YjsDatabaseKey.id): GroupId;
|
||||
|
||||
|
@ -1,2 +1,16 @@
|
||||
import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export const DEFAULT_ROW_HEIGHT = 37;
|
||||
export const MIN_COLUMN_WIDTH = 100;
|
||||
|
||||
export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
|
||||
const rowMeta = rowMetas.get(rowId);
|
||||
const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
||||
|
||||
return meta?.get(YjsDatabaseKey.cells)?.get(fieldId);
|
||||
};
|
||||
|
||||
export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
|
||||
return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data);
|
||||
};
|
||||
|
@ -49,3 +49,17 @@ export interface Filter {
|
||||
id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export enum CalendarLayout {
|
||||
MonthLayout = 0,
|
||||
WeekLayout = 1,
|
||||
DayLayout = 2,
|
||||
}
|
||||
|
||||
export interface CalendarLayoutSetting {
|
||||
fieldId: string;
|
||||
firstDayOfWeek: number;
|
||||
showWeekNumbers: boolean;
|
||||
showWeekends: boolean;
|
||||
layout: CalendarLayout;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export interface ChecklistCellData {
|
||||
export function parseChecklistData(data: string): ChecklistCellData | null {
|
||||
try {
|
||||
const { options, selected_option_ids } = JSON.parse(data);
|
||||
const percentage = (selected_option_ids.length / options.length) * 100;
|
||||
const percentage = selected_option_ids.length / options.length;
|
||||
|
||||
return {
|
||||
percentage,
|
||||
|
@ -181,10 +181,10 @@ export function checklistFilterCheck(data: string, content: string, condition: n
|
||||
const percentage = parseChecklistData(data)?.percentage ?? 0;
|
||||
|
||||
if (condition === ChecklistFilterCondition.IsComplete) {
|
||||
return percentage === 100;
|
||||
return percentage === 1;
|
||||
}
|
||||
|
||||
return percentage !== 100;
|
||||
return percentage !== 1;
|
||||
}
|
||||
|
||||
export function selectOptionFilterCheck(data: string, content: string, condition: number) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { YDatabaseField, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import { YDatabaseField, YDoc, YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { getCellData } from '@/application/database-yjs/const';
|
||||
import { FieldType } from '@/application/database-yjs/database.type';
|
||||
import { parseSelectOptionTypeOptions } from '@/application/database-yjs/fields';
|
||||
import { Row } from '@/application/database-yjs/selector';
|
||||
@ -12,13 +13,6 @@ export function groupByField(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabas
|
||||
return groupBySelectOption(rows, rowMetas, field);
|
||||
}
|
||||
|
||||
function getCellData(rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) {
|
||||
const rowMeta = rowMetas.get(rowId);
|
||||
const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
||||
|
||||
return meta?.get(YjsDatabaseKey.cells)?.get(fieldId)?.get(YjsDatabaseKey.data);
|
||||
}
|
||||
|
||||
export function groupBySelectOption(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabaseField) {
|
||||
const fieldId = field.get(YjsDatabaseKey.id);
|
||||
const result = new Map<string, Row[]>();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsFolderKey } from '@/application/collab.type';
|
||||
import { getCell, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||
import {
|
||||
DatabaseContext,
|
||||
useDatabase,
|
||||
@ -13,10 +13,13 @@ import { filterBy, parseFilter } from '@/application/database-yjs/filter';
|
||||
import { groupByField } from '@/application/database-yjs/group';
|
||||
import { sortBy } from '@/application/database-yjs/sort';
|
||||
import { useViewsIdSelector } from '@/application/folder-yjs';
|
||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
||||
import { DateTimeCell } from '@/components/database/components/cell/cell.type';
|
||||
import dayjs from 'dayjs';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type';
|
||||
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, SortCondition } from './database.type';
|
||||
|
||||
export interface Column {
|
||||
fieldId: string;
|
||||
@ -34,7 +37,8 @@ const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmp
|
||||
|
||||
export function useDatabaseViewsSelector() {
|
||||
const database = useDatabase();
|
||||
const { viewsId: visibleViewsId } = useViewsIdSelector();
|
||||
const { objectId: currentViewId } = useId();
|
||||
const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector();
|
||||
const views = database?.get(YjsDatabaseKey.views);
|
||||
const [viewIds, setViewIds] = useState<string[]>([]);
|
||||
const childViews = useMemo(() => {
|
||||
@ -45,7 +49,16 @@ export function useDatabaseViewsSelector() {
|
||||
if (!views) return;
|
||||
|
||||
const observerEvent = () => {
|
||||
setViewIds(Array.from(views.keys()).filter((id) => visibleViewsId.includes(id)));
|
||||
setViewIds(
|
||||
Array.from(views.keys()).filter((id) => {
|
||||
const view = folderViews?.get(id);
|
||||
|
||||
return (
|
||||
visibleViewsId.includes(id) &&
|
||||
(view?.get(YjsFolderKey.bid) === currentViewId || view?.get(YjsFolderKey.id) === currentViewId)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
observerEvent();
|
||||
@ -54,7 +67,7 @@ export function useDatabaseViewsSelector() {
|
||||
return () => {
|
||||
views.unobserve(observerEvent);
|
||||
};
|
||||
}, [visibleViewsId, views]);
|
||||
}, [visibleViewsId, views, folderViews, currentViewId]);
|
||||
|
||||
return {
|
||||
childViews,
|
||||
@ -478,3 +491,97 @@ export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: st
|
||||
|
||||
return cellValue;
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
start?: Date;
|
||||
end?: Date;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function useCalendarEventsSelector() {
|
||||
const setting = useCalendarLayoutSetting();
|
||||
const filedId = setting.fieldId;
|
||||
const { field } = useFieldSelector(filedId);
|
||||
const rowOrders = useRowOrdersSelector();
|
||||
const rows = useContext(DatabaseContext)?.rowDocMap;
|
||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||
const [emptyEvents, setEmptyEvents] = useState<CalendarEvent[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!field || !rowOrders || !rows) return;
|
||||
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
||||
|
||||
if (fieldType !== FieldType.DateTime) return;
|
||||
const newEvents: CalendarEvent[] = [];
|
||||
const emptyEvents: CalendarEvent[] = [];
|
||||
|
||||
rowOrders?.forEach((row) => {
|
||||
const cell = getCell(row.id, filedId, rows);
|
||||
|
||||
if (!cell) {
|
||||
emptyEvents.push({
|
||||
id: `${row.id}:${filedId}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const value = parseYDatabaseCellToCell(cell) as DateTimeCell;
|
||||
|
||||
if (!value || !value.data) {
|
||||
emptyEvents.push({
|
||||
id: `${row.id}:${filedId}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const getDate = (timestamp: string) => {
|
||||
const dayjsResult = timestamp.length === 10 ? dayjs.unix(Number(timestamp)) : dayjs(timestamp);
|
||||
|
||||
return dayjsResult.toDate();
|
||||
};
|
||||
|
||||
newEvents.push({
|
||||
id: `${row.id}:${filedId}`,
|
||||
start: getDate(value.data),
|
||||
end: value.endTimestamp && value.isRange ? getDate(value.endTimestamp) : getDate(value.data),
|
||||
});
|
||||
});
|
||||
|
||||
setEvents(newEvents);
|
||||
setEmptyEvents(emptyEvents);
|
||||
}, [field, rowOrders, rows, filedId]);
|
||||
|
||||
return { events, emptyEvents };
|
||||
}
|
||||
|
||||
export function useCalendarLayoutSetting() {
|
||||
const view = useDatabaseView();
|
||||
const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('2');
|
||||
const [setting, setSetting] = useState<CalendarLayoutSetting>({
|
||||
fieldId: '',
|
||||
firstDayOfWeek: 0,
|
||||
showWeekNumbers: true,
|
||||
showWeekends: true,
|
||||
layout: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const observerHandler = () => {
|
||||
setSetting({
|
||||
fieldId: layoutSetting?.get(YjsDatabaseKey.field_id) as string,
|
||||
firstDayOfWeek: Number(layoutSetting?.get(YjsDatabaseKey.first_day_of_week)),
|
||||
showWeekNumbers: Boolean(layoutSetting?.get(YjsDatabaseKey.show_week_numbers)),
|
||||
showWeekends: Boolean(layoutSetting?.get(YjsDatabaseKey.show_weekends)),
|
||||
layout: Number(layoutSetting?.get(YjsDatabaseKey.layout_ty)),
|
||||
});
|
||||
};
|
||||
|
||||
observerHandler();
|
||||
layoutSetting?.observe(observerHandler);
|
||||
return () => {
|
||||
layoutSetting?.unobserve(observerHandler);
|
||||
};
|
||||
}, [layoutSetting]);
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
@ -5,38 +5,39 @@ import { useEffect, useState } from 'react';
|
||||
export function useViewsIdSelector() {
|
||||
const folder = useFolderContext();
|
||||
const [viewsId, setViewsId] = useState<string[]>([]);
|
||||
const views = folder?.get(YjsFolderKey.views);
|
||||
const trash = folder?.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
|
||||
const meta = folder?.get(YjsFolderKey.meta);
|
||||
|
||||
useEffect(() => {
|
||||
if (!folder) return;
|
||||
if (!views) return;
|
||||
|
||||
const views = folder.get(YjsFolderKey.views);
|
||||
const trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
|
||||
const meta = folder.get(YjsFolderKey.meta);
|
||||
const trashUid = Array.from(trash?.keys())[0];
|
||||
const userTrash = trash?.get(trashUid);
|
||||
const trashUid = trash ? Array.from(trash.keys())[0] : null;
|
||||
const userTrash = trashUid ? trash?.get(trashUid) : null;
|
||||
|
||||
const collectIds = () => {
|
||||
const trashIds = userTrash?.toJSON()?.map((item) => item.id) || [];
|
||||
|
||||
return Array.from(views.keys()).filter(
|
||||
(id) => !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace)
|
||||
);
|
||||
return Array.from(views.keys()).filter((id) => {
|
||||
return !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace);
|
||||
});
|
||||
};
|
||||
|
||||
setViewsId(collectIds());
|
||||
const observerEvent = () => setViewsId(collectIds());
|
||||
|
||||
folder.observe(observerEvent);
|
||||
userTrash.observe(observerEvent);
|
||||
views.observe(observerEvent);
|
||||
userTrash?.observe(observerEvent);
|
||||
|
||||
return () => {
|
||||
folder.unobserve(observerEvent);
|
||||
userTrash.unobserve(observerEvent);
|
||||
views.unobserve(observerEvent);
|
||||
userTrash?.unobserve(observerEvent);
|
||||
};
|
||||
}, [folder]);
|
||||
}, [views, trash, meta]);
|
||||
|
||||
return {
|
||||
viewsId,
|
||||
views,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,63 @@
|
||||
import { Box, ClickAwayListener, Fade, Paper, Popper, PopperPlacementType } from '@mui/material';
|
||||
import React, { ReactElement, useEffect } from 'react';
|
||||
|
||||
interface Props {
|
||||
content: ReactElement;
|
||||
children: ReactElement;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
placement?: PopperPlacementType;
|
||||
}
|
||||
|
||||
export const RichTooltip = ({ placement = 'top', open, onClose, content, children }: Props) => {
|
||||
const [childNode, setChildNode] = React.useState<HTMLElement | null>(null);
|
||||
const [, setTransitioning] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTransitioning(true);
|
||||
}
|
||||
}, [open]);
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, { ...children.props, ref: setChildNode })}
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={childNode}
|
||||
placement={placement}
|
||||
transition
|
||||
style={{ zIndex: 2000 }}
|
||||
modifiers={[
|
||||
{
|
||||
name: 'flip',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
enabled: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade
|
||||
{...TransitionProps}
|
||||
timeout={350}
|
||||
onTransitionEnd={() => {
|
||||
setTransitioning(false);
|
||||
}}
|
||||
>
|
||||
<Paper className={'bg-transparent shadow-none'}>
|
||||
<ClickAwayListener onClickAway={onClose}>
|
||||
<Paper className={'m-2 rounded-md border border-line-divider bg-bg-body'}>
|
||||
<Box>{content}</Box>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichTooltip;
|
@ -1 +1,2 @@
|
||||
export * from './Popover';
|
||||
export * from './RichTooltip';
|
||||
|
@ -3,10 +3,9 @@ import { useDatabaseViewsSelector } from '@/application/database-yjs';
|
||||
import { Board } from '@/components/database/board';
|
||||
import { Calendar } from '@/components/database/calendar';
|
||||
import { DatabaseConditionsContext } from '@/components/database/components/conditions/context';
|
||||
import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs';
|
||||
import { DatabaseTabs } from '@/components/database/components/tabs';
|
||||
import { Grid } from '@/components/database/grid';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import SwipeableViews from 'react-swipeable-views';
|
||||
import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions';
|
||||
|
||||
function DatabaseViews({
|
||||
@ -25,22 +24,29 @@ function DatabaseViews({
|
||||
);
|
||||
}, [currentViewId, viewIds]);
|
||||
|
||||
const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => {
|
||||
switch (layout) {
|
||||
case DatabaseViewLayout.Grid:
|
||||
return Grid;
|
||||
case DatabaseViewLayout.Board:
|
||||
return Board;
|
||||
case DatabaseViewLayout.Calendar:
|
||||
return Calendar;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [conditionsExpanded, setConditionsExpanded] = useState<boolean>(false);
|
||||
const toggleExpanded = useCallback(() => {
|
||||
setConditionsExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const activeView = useMemo(() => {
|
||||
return childViews[value];
|
||||
}, [childViews, value]);
|
||||
|
||||
const view = useMemo(() => {
|
||||
if (!activeView) return null;
|
||||
const layout = Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
||||
|
||||
switch (layout) {
|
||||
case DatabaseViewLayout.Grid:
|
||||
return <Grid />;
|
||||
case DatabaseViewLayout.Board:
|
||||
return <Board />;
|
||||
case DatabaseViewLayout.Calendar:
|
||||
return <Calendar />;
|
||||
}
|
||||
}, [activeView]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DatabaseConditionsContext.Provider
|
||||
@ -52,33 +58,7 @@ function DatabaseViews({
|
||||
<DatabaseTabs selectedViewId={currentViewId} setSelectedViewId={onChangeView} viewIds={viewIds} />
|
||||
<DatabaseConditions />
|
||||
</DatabaseConditionsContext.Provider>
|
||||
<SwipeableViews
|
||||
slideStyle={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
className={'h-full w-full flex-1 overflow-hidden'}
|
||||
axis={'x'}
|
||||
index={value}
|
||||
containerStyle={{ height: '100%' }}
|
||||
>
|
||||
{childViews.map((view, index) => {
|
||||
const layout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
||||
const Component = getDatabaseViewComponent(layout);
|
||||
const viewId = viewIds[index];
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
data-view-id={viewId}
|
||||
className={'flex h-full w-full flex-col'}
|
||||
key={viewId}
|
||||
index={index}
|
||||
value={value}
|
||||
>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</SwipeableViews>
|
||||
<div className={'flex h-full w-full flex-1 flex-col overflow-hidden'}>{view}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { useCalendarEventsSelector, useCalendarLayoutSetting } from '@/application/database-yjs';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { dayjsLocalizer } from 'react-big-calendar';
|
||||
import dayjs from 'dayjs';
|
||||
import en from 'dayjs/locale/en';
|
||||
|
||||
export function useCalendarSetup() {
|
||||
const layoutSetting = useCalendarLayoutSetting();
|
||||
const { events, emptyEvents } = useCalendarEventsSelector();
|
||||
|
||||
const dayPropGetter = useCallback((date: Date) => {
|
||||
const day = date.getDay();
|
||||
|
||||
return {
|
||||
className: `day-${day}`,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dayjs.locale({
|
||||
...en,
|
||||
weekStart: layoutSetting.firstDayOfWeek,
|
||||
});
|
||||
}, [layoutSetting]);
|
||||
|
||||
const localizer = useMemo(() => dayjsLocalizer(dayjs), []);
|
||||
|
||||
const formats = useMemo(() => {
|
||||
return {
|
||||
weekdayFormat: 'ddd',
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
localizer,
|
||||
formats,
|
||||
dayPropGetter,
|
||||
events,
|
||||
emptyEvents,
|
||||
};
|
||||
}
|
@ -1,15 +1,32 @@
|
||||
import { AFScroller } from '@/components/_shared/scroller';
|
||||
import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks';
|
||||
import { Toolbar, Event } from '@/components/database/components/calendar';
|
||||
import React from 'react';
|
||||
import { Calendar as BigCalendar, dayjsLocalizer } from 'react-big-calendar';
|
||||
import dayjs from 'dayjs';
|
||||
import { Calendar as BigCalendar } from 'react-big-calendar';
|
||||
import './calendar.scss';
|
||||
|
||||
const localizer = dayjsLocalizer(dayjs);
|
||||
|
||||
export function Calendar() {
|
||||
const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup();
|
||||
|
||||
return (
|
||||
<div className={'max-ms:px-4 px-24 py-4'}>
|
||||
<BigCalendar localizer={localizer} startAccessor='start' endAccessor='end' style={{ height: 500 }} />
|
||||
</div>
|
||||
<AFScroller className={'appflowy-calendar'}>
|
||||
<div className={'h-full max-h-[960px] min-h-[560px] px-24 py-4 max-md:px-4'}>
|
||||
<BigCalendar
|
||||
components={{
|
||||
toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />,
|
||||
eventWrapper: Event,
|
||||
}}
|
||||
events={events}
|
||||
views={['month']}
|
||||
localizer={localizer}
|
||||
formats={formats}
|
||||
dayPropGetter={dayPropGetter}
|
||||
showMultiDayTimes={true}
|
||||
step={1}
|
||||
showAllEvents={true}
|
||||
/>
|
||||
</div>
|
||||
</AFScroller>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,63 @@
|
||||
$today-highlight-bg: transparent;
|
||||
@import 'react-big-calendar/lib/sass/styles';
|
||||
@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD
|
||||
@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD
|
||||
|
||||
.rbc-calendar {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rbc-button-link {
|
||||
@apply rounded-full w-[20px] h-[20px] my-1.5;
|
||||
}
|
||||
|
||||
.rbc-date-cell {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.rbc-date-cell.rbc-now {
|
||||
|
||||
color: var(--content-on-fill);
|
||||
|
||||
.rbc-button-link {
|
||||
background-color: var(--function-error);
|
||||
}
|
||||
}
|
||||
|
||||
.rbc-month-view {
|
||||
border: none;
|
||||
|
||||
.rbc-month-row {
|
||||
border: 1px solid var(--line-divider);
|
||||
border-bottom: none;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid var(--line-divider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rbc-month-header {
|
||||
height: 40px;
|
||||
|
||||
.rbc-header {
|
||||
border: none;
|
||||
@apply flex items-end py-2 justify-center font-normal text-text-caption;
|
||||
}
|
||||
}
|
||||
|
||||
.rbc-month-row .rbc-row-bg {
|
||||
.rbc-off-range-bg {
|
||||
background-color: transparent;
|
||||
color: var(--text-caption);
|
||||
}
|
||||
|
||||
.rbc-day-bg.day-0, .rbc-day-bg.day-6 {
|
||||
background-color: var(--fill-list-active);
|
||||
}
|
||||
}
|
||||
|
||||
.rbc-month-row {
|
||||
display: inline-table !important;
|
||||
flex: 0 0 0 !important;
|
||||
min-height: 120px !important;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { useFieldsSelector } from '@/application/database-yjs';
|
||||
import CardField from '@/components/database/components/board/card/CardField';
|
||||
import CardField from '@/components/database/components/field/CardField';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
export interface CardProps {
|
||||
|
@ -26,18 +26,7 @@ export const Group = ({ groupId }: GroupProps) => {
|
||||
if (columns.length === 0 || !fieldId) return null;
|
||||
return (
|
||||
<AFScroller overflowYHidden className={'relative px-24 max-md:px-4'}>
|
||||
<Droppable
|
||||
droppableId={`group-${groupId}`}
|
||||
direction='horizontal'
|
||||
type='column'
|
||||
renderClone={(provided, snapshot, rubric) => {
|
||||
// we have a transform: * on one of the parents of a <Draggable /> then the positioning logic will be incorrect while dragging
|
||||
// https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md
|
||||
const id = columns[rubric.source.index].id;
|
||||
|
||||
return <Column key={id} rows={groupResult.get(id)} provided={provided} id={id} fieldId={fieldId} />;
|
||||
}}
|
||||
>
|
||||
<Droppable droppableId={`group-${groupId}`} direction='horizontal' type='column'>
|
||||
{(provided) => {
|
||||
return (
|
||||
<div
|
||||
@ -60,6 +49,7 @@ export const Group = ({ groupId }: GroupProps) => {
|
||||
}}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { CalendarEvent, useFieldsSelector } from '@/application/database-yjs';
|
||||
import { RichTooltip } from '@/components/_shared/popover';
|
||||
import EventPaper from '@/components/database/components/calendar/event/EventPaper';
|
||||
import CardField from '@/components/database/components/field/CardField';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EventWrapperProps } from 'react-big-calendar';
|
||||
|
||||
export function Event({ event }: EventWrapperProps<CalendarEvent>) {
|
||||
const { id } = event;
|
||||
const [rowId, fieldId] = id.split(':');
|
||||
const fields = useFieldsSelector();
|
||||
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]);
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className={'px-1 py-0.5'}>
|
||||
<RichTooltip content={<EventPaper rowId={rowId} />} open={open} placement='right' onClose={() => setOpen(false)}>
|
||||
<div
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className={
|
||||
'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 shadow-sm hover:bg-fill-list-active hover:shadow'
|
||||
}
|
||||
>
|
||||
{showFields.map((field) => {
|
||||
return <CardField index={0} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
||||
})}
|
||||
</div>
|
||||
</RichTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Event;
|
@ -0,0 +1,28 @@
|
||||
import { useFieldsSelector } from '@/application/database-yjs';
|
||||
import { Property } from '@/components/database/components/property';
|
||||
import { IconButton } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
||||
|
||||
function EventPaper({ rowId }: { rowId: string }) {
|
||||
const fields = useFieldsSelector();
|
||||
|
||||
return (
|
||||
<div className={'max-h-[260px] w-[360px] overflow-y-auto'}>
|
||||
<div className={'flex h-fit w-full flex-col items-center justify-center py-2 px-3'}>
|
||||
<div className={'flex w-full items-center justify-end'}>
|
||||
<IconButton size={'small'}>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={'flex w-full flex-1 flex-col gap-4 overflow-y-auto py-2'}>
|
||||
{fields.map((field) => {
|
||||
return <Property fieldId={field.fieldId} rowId={rowId} key={field.fieldId} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventPaper;
|
@ -0,0 +1 @@
|
||||
export * from './Event';
|
@ -0,0 +1,2 @@
|
||||
export * from './toolbar';
|
||||
export * from './event';
|
@ -0,0 +1,46 @@
|
||||
import { CalendarEvent } from '@/application/database-yjs';
|
||||
import { RichTooltip } from '@/components/_shared/popover';
|
||||
import NoDateRow from '@/components/database/components/calendar/toolbar/NoDateRow';
|
||||
import Button from '@mui/material/Button';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
const content = useMemo(() => {
|
||||
return (
|
||||
<div className={'flex w-[260px] flex-col gap-3 p-2 text-xs font-medium'}>
|
||||
<div className={'text-text-caption'}>{t('calendar.settings.clickToOpen')}</div>
|
||||
{emptyEvents.map((event) => {
|
||||
const rowId = event.id.split(':')[0];
|
||||
|
||||
return <NoDateRow rowId={rowId} key={event.id} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}, [emptyEvents, t]);
|
||||
|
||||
return (
|
||||
<RichTooltip
|
||||
content={content}
|
||||
open={open}
|
||||
placement={'bottom'}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'outlined'}
|
||||
className={'rounded-md border-line-divider '}
|
||||
color={'inherit'}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{`${t('calendar.settings.noDateTitle')} (${emptyEvents.length})`}
|
||||
</Button>
|
||||
</RichTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoDate;
|
@ -0,0 +1,44 @@
|
||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { useCellSelector, useDatabase } from '@/application/database-yjs';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Cell from 'src/components/database/components/cell/Cell';
|
||||
|
||||
function NoDateRow({ rowId }: { rowId: string }) {
|
||||
const database = useDatabase();
|
||||
const [primaryFieldId, setPrimaryFieldId] = React.useState<string | null>(null);
|
||||
const cell = useCellSelector({
|
||||
rowId,
|
||||
fieldId: primaryFieldId || '',
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const fields = database?.get(YjsDatabaseKey.fields);
|
||||
const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => {
|
||||
return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary);
|
||||
});
|
||||
|
||||
setPrimaryFieldId(primaryFieldId || null);
|
||||
}, [database]);
|
||||
|
||||
if (!primaryFieldId || !cell?.data) {
|
||||
return <div className={'text-xs text-text-caption'}>{t('grid.row.titlePlaceholder')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'w-full hover:text-fill-default'}>
|
||||
<Cell
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
readOnly
|
||||
cell={cell}
|
||||
rowId={rowId}
|
||||
fieldId={primaryFieldId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoDateRow;
|
@ -0,0 +1,59 @@
|
||||
import { CalendarEvent } from '@/application/database-yjs';
|
||||
import NoDate from '@/components/database/components/calendar/toolbar/NoDate';
|
||||
import { IconButton } from '@mui/material';
|
||||
import Button from '@mui/material/Button';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ToolbarProps } from 'react-big-calendar';
|
||||
import { ReactComponent as LeftArrow } from '$icons/16x/arrow_left.svg';
|
||||
import { ReactComponent as RightArrow } from '$icons/16x/arrow_right.svg';
|
||||
import { ReactComponent as DownArrow } from '$icons/16x/arrow_down.svg';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function Toolbar({
|
||||
onNavigate,
|
||||
date,
|
||||
emptyEvents,
|
||||
}: ToolbarProps & {
|
||||
emptyEvents: CalendarEvent[];
|
||||
}) {
|
||||
const dateStr = useMemo(() => dayjs(date).format('MMM YYYY'), [date]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<div className={'text-sm font-medium'}>{dateStr}</div>
|
||||
<div className={'flex items-center justify-end gap-2'}>
|
||||
<IconButton size={'small'} onClick={() => onNavigate('PREV')}>
|
||||
<LeftArrow />
|
||||
</IconButton>
|
||||
<Button
|
||||
className={'h-6 font-normal'}
|
||||
size={'small'}
|
||||
variant={'text'}
|
||||
color={'inherit'}
|
||||
onClick={() => onNavigate('TODAY')}
|
||||
>
|
||||
{t('calendar.navigation.today')}
|
||||
</Button>
|
||||
<IconButton size={'small'} onClick={() => onNavigate('NEXT')}>
|
||||
<RightArrow />
|
||||
</IconButton>
|
||||
<Button
|
||||
size={'small'}
|
||||
variant={'outlined'}
|
||||
className={'rounded-md border-line-divider'}
|
||||
color={'inherit'}
|
||||
onClick={() => onNavigate('TODAY')}
|
||||
endIcon={<DownArrow className={'h-3 w-3 text-text-caption'} />}
|
||||
>
|
||||
{t('calendar.navigation.views.month')}
|
||||
</Button>
|
||||
<NoDate emptyEvents={emptyEvents} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Toolbar;
|
@ -0,0 +1 @@
|
||||
export * from './Toolbar';
|
@ -81,4 +81,6 @@ export interface CellProps<T extends Cell> {
|
||||
fieldId: FieldId;
|
||||
style?: React.CSSProperties;
|
||||
readOnly?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export function ChecklistCell({ cell, style }: CellProps<ChecklistCellType>) {
|
||||
|
||||
if (!data || !options || !selectedOptions) return null;
|
||||
return (
|
||||
<div style={style} className={'cursor-pointer'}>
|
||||
<div style={style} className={'w-full cursor-pointer'}>
|
||||
<LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} />
|
||||
</div>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/databa
|
||||
import React, { useMemo } from 'react';
|
||||
import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg';
|
||||
|
||||
export function DateTimeCell({ cell, fieldId, style }: CellProps<DateTimeCellType>) {
|
||||
export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps<DateTimeCellType>) {
|
||||
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
|
||||
|
||||
const startDateTime = useMemo(() => {
|
||||
@ -25,7 +25,12 @@ export function DateTimeCell({ cell, fieldId, style }: CellProps<DateTimeCellTyp
|
||||
|
||||
const hasReminder = !!cell?.reminderId;
|
||||
|
||||
if (!cell?.data) return null;
|
||||
if (!cell?.data)
|
||||
return placeholder ? (
|
||||
<div style={style} className={'text-text-placeholder'}>
|
||||
{placeholder}
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
<div style={style} className={'flex cursor-text items-center gap-1'}>
|
||||
{hasReminder && <ReminderSvg className={'h-4 w-4'} />}
|
||||
|
@ -3,7 +3,7 @@ import { CellProps, NumberCell as NumberCellType } from '@/components/database/c
|
||||
import React, { useMemo } from 'react';
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export function NumberCell({ cell, fieldId, style }: CellProps<NumberCellType>) {
|
||||
export function NumberCell({ cell, fieldId, style, placeholder }: CellProps<NumberCellType>) {
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
|
||||
const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]);
|
||||
@ -22,7 +22,12 @@ export function NumberCell({ cell, fieldId, style }: CellProps<NumberCellType>)
|
||||
return numberFormater(new Decimal(cell.data).toNumber());
|
||||
}, [cell, format]);
|
||||
|
||||
if (value === undefined) return null;
|
||||
if (value === undefined)
|
||||
return placeholder ? (
|
||||
<div style={style} className={'text-text-placeholder'}>
|
||||
{placeholder}
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
<div style={style} className={className}>
|
||||
{value}
|
||||
|
@ -1,7 +1,13 @@
|
||||
import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type';
|
||||
import RelationItems from '@/components/database/components/cell/relation/RelationItems';
|
||||
import React from 'react';
|
||||
|
||||
export function RelationCell({ cell, fieldId, style }: CellProps<RelationCellType>) {
|
||||
if (!cell?.data) return null;
|
||||
export function RelationCell({ cell, fieldId, style, placeholder }: CellProps<RelationCellType>) {
|
||||
if (!cell?.data)
|
||||
return placeholder ? (
|
||||
<div style={style} className={'text-text-placeholder'}>
|
||||
{placeholder}
|
||||
</div>
|
||||
) : null;
|
||||
return <RelationItems cell={cell} fieldId={fieldId} style={style} />;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { SelectOptionColorMap } from '@/components/database/components/cell/cell
|
||||
import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/components/database/components/cell/cell.type';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
export function SelectOptionCell({ cell, fieldId, style }: CellProps<SelectOptionCellType>) {
|
||||
export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps<SelectOptionCellType>) {
|
||||
const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]);
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const typeOption = useMemo(() => {
|
||||
@ -18,12 +18,17 @@ export function SelectOptionCell({ cell, fieldId, style }: CellProps<SelectOptio
|
||||
const option = typeOption?.options?.find((option) => option.id === id);
|
||||
|
||||
if (!option) return null;
|
||||
return <Tag key={option.id} color={SelectOptionColorMap[option.color]} label={option.name} />;
|
||||
return <Tag key={option.id} size={'small'} color={SelectOptionColorMap[option.color]} label={option.name} />;
|
||||
}),
|
||||
[typeOption]
|
||||
);
|
||||
|
||||
if (!typeOption || !selectOptionIds?.length) return null;
|
||||
if (!typeOption || !selectOptionIds?.length)
|
||||
return placeholder ? (
|
||||
<div style={style} className={'text-text-placeholder'}>
|
||||
{placeholder}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div style={style} className={'flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'}>
|
||||
|
@ -3,13 +3,13 @@ import { CellProps, UrlCell as UrlCellType } from '@/components/database/compone
|
||||
import { openUrl, processUrl } from '@/utils/url';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
export function UrlCell({ cell, style }: CellProps<UrlCellType>) {
|
||||
export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
|
||||
const readOnly = useReadOnly();
|
||||
|
||||
const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]);
|
||||
|
||||
const className = useMemo(() => {
|
||||
const classList = ['select-text'];
|
||||
const classList = ['select-text', 'w-fit'];
|
||||
|
||||
if (isUrl) {
|
||||
classList.push('text-content-blue-400', 'underline', 'cursor-pointer');
|
||||
@ -20,7 +20,12 @@ export function UrlCell({ cell, style }: CellProps<UrlCellType>) {
|
||||
return classList.join(' ');
|
||||
}, [isUrl]);
|
||||
|
||||
if (!cell?.data) return null;
|
||||
if (!cell?.data)
|
||||
return placeholder ? (
|
||||
<div style={style} className={'text-text-placeholder'}>
|
||||
{placeholder}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs';
|
||||
import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { useDatabaseView, useFiltersSelector, useSortsSelector } from '@/application/database-yjs';
|
||||
import { useConditionsContext } from '@/components/database/components/conditions/context';
|
||||
import { TextButton } from '@/components/database/components/tabs/TextButton';
|
||||
import React from 'react';
|
||||
@ -6,10 +7,16 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function DatabaseActions() {
|
||||
const { t } = useTranslation();
|
||||
const view = useDatabaseView();
|
||||
const layout = Number(view?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
||||
const sorts = useSortsSelector();
|
||||
const filter = useFiltersSelector();
|
||||
const conditionsContext = useConditionsContext();
|
||||
|
||||
if (layout === DatabaseViewLayout.Calendar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex w-[120px] items-center justify-end gap-1.5'>
|
||||
<TextButton
|
||||
|
@ -0,0 +1,78 @@
|
||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs';
|
||||
import { Cell as CellType, CellProps, TextCell } from '@/components/database/components/cell/cell.type';
|
||||
import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
||||
import { ChecklistCell } from '@/components/database/components/cell/checklist';
|
||||
import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified';
|
||||
import { DateTimeCell } from '@/components/database/components/cell/date';
|
||||
import { NumberCell } from '@/components/database/components/cell/number';
|
||||
import { RelationCell } from '@/components/database/components/cell/relation';
|
||||
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
|
||||
import { UrlCell } from '@/components/database/components/cell/url';
|
||||
import PropertyWrapper from '@/components/database/components/property/PropertyWrapper';
|
||||
import { TextProperty } from '@/components/database/components/property/text';
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function Property({ fieldId, rowId }: { fieldId: string; rowId: string }) {
|
||||
const cell = useCellSelector({
|
||||
fieldId,
|
||||
rowId,
|
||||
});
|
||||
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const Component = useMemo(() => {
|
||||
switch (fieldType) {
|
||||
case FieldType.URL:
|
||||
return UrlCell;
|
||||
case FieldType.Number:
|
||||
return NumberCell;
|
||||
case FieldType.Checkbox:
|
||||
return CheckboxCell;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return SelectOptionCell;
|
||||
case FieldType.DateTime:
|
||||
return DateTimeCell;
|
||||
case FieldType.Checklist:
|
||||
return ChecklistCell;
|
||||
case FieldType.Relation:
|
||||
return RelationCell;
|
||||
default:
|
||||
return TextProperty;
|
||||
}
|
||||
}, [fieldType]) as FC<CellProps<CellType>>;
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
fontSize: '12px',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
if (fieldType === FieldType.RichText) {
|
||||
return <TextProperty cell={cell as TextCell} fieldId={fieldId} rowId={rowId} />;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) {
|
||||
const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified;
|
||||
|
||||
return (
|
||||
<PropertyWrapper fieldId={fieldId}>
|
||||
<RowCreateModifiedTime style={style} rowId={rowId} fieldId={fieldId} attrName={attrName} />
|
||||
</PropertyWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PropertyWrapper fieldId={fieldId}>
|
||||
<Component cell={cell} style={style} placeholder={t('grid.row.textPlaceholder')} fieldId={fieldId} rowId={rowId} />
|
||||
</PropertyWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Property;
|
@ -0,0 +1,15 @@
|
||||
import { FieldDisplay } from '@/components/database/components/field';
|
||||
import React from 'react';
|
||||
|
||||
function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={'flex w-full items-center gap-2'}>
|
||||
<div className={'w-[100px] text-text-caption'}>
|
||||
<FieldDisplay fieldId={fieldId} />
|
||||
</div>
|
||||
<div className={'flex flex-1 flex-wrap pr-1'}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PropertyWrapper;
|
@ -0,0 +1 @@
|
||||
export * from './Property';
|
@ -0,0 +1,29 @@
|
||||
import { CellProps, TextCell } from '@/components/database/components/cell/cell.type';
|
||||
import { TextField } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export function TextProperty({ cell }: CellProps<TextCell>) {
|
||||
return (
|
||||
<TextField
|
||||
value={cell?.data}
|
||||
inputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
fullWidth
|
||||
size={'small'}
|
||||
sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
fontSize: '0.875rem',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
|
||||
'& .MuiInputBase-input': {
|
||||
padding: '4px 8px',
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextProperty;
|
@ -0,0 +1 @@
|
||||
export * from './TextProperty';
|
@ -69,7 +69,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
||||
value={isSelected ? selectedViewId : objectId}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{viewIds.map((viewId, index) => {
|
||||
{viewIds.map((viewId) => {
|
||||
const view = getFolderView(viewId);
|
||||
|
||||
if (!view) return null;
|
||||
@ -80,9 +80,6 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
||||
return (
|
||||
<ViewTab
|
||||
key={viewId}
|
||||
style={{
|
||||
borderRight: index === viewIds.length - 1 ? 'none' : '1px solid var(--line-divider)',
|
||||
}}
|
||||
icon={<Icon className={'h-4 w-4'} />}
|
||||
iconPosition='start'
|
||||
color='inherit'
|
||||
|
@ -45,7 +45,7 @@
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
.workspaces, .database-conditions, .grid-scroll-table, .grid-board {
|
||||
.workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database {
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
@ -79,3 +79,22 @@
|
||||
@apply items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-arrow {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
width: 1em;
|
||||
height: 0.71em;
|
||||
color: var(--bg-body);
|
||||
|
||||
&:before {
|
||||
content: '""';
|
||||
margin: auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
boxShadow: var(--shadow);
|
||||
backgroundColor: var(--bg-body);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
@ -1266,7 +1266,13 @@
|
||||
"today": "Today",
|
||||
"jumpToday": "Jump to Today",
|
||||
"previousMonth": "Previous Month",
|
||||
"nextMonth": "Next Month"
|
||||
"nextMonth": "Next Month",
|
||||
"views": {
|
||||
"day": "Day",
|
||||
"week": "Week",
|
||||
"month": "Month",
|
||||
"year": "Year"
|
||||
}
|
||||
},
|
||||
"mobileEventScreen": {
|
||||
"emptyTitle": "No events yet",
|
||||
@ -1286,7 +1292,8 @@
|
||||
},
|
||||
"unscheduledEventsTitle": "Unscheduled events",
|
||||
"clickToAdd": "Click to add to the calendar",
|
||||
"name": "Calendar settings"
|
||||
"name": "Calendar settings",
|
||||
"clickToOpen": "Click to open the record"
|
||||
},
|
||||
"referencedCalendarPrefix": "View of",
|
||||
"quickJumpYear": "Jump to",
|
||||
|
Loading…
Reference in New Issue
Block a user