mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 04:04:27 +08:00
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
DocsService,
|
||||||
type Framework,
|
type Framework,
|
||||||
WorkspaceScope,
|
WorkspaceScope,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
@@ -6,6 +7,7 @@ import {
|
|||||||
|
|
||||||
import { WorkspaceDialogService } from '../dialogs';
|
import { WorkspaceDialogService } from '../dialogs';
|
||||||
import { DocDisplayMetaService } from '../doc-display-meta';
|
import { DocDisplayMetaService } from '../doc-display-meta';
|
||||||
|
import { EditorSettingService } from '../editor-setting';
|
||||||
import { JournalService } from '../journal';
|
import { JournalService } from '../journal';
|
||||||
import { RecentDocsService } from '../quicksearch';
|
import { RecentDocsService } from '../quicksearch';
|
||||||
import { AtMenuConfigService } from './services';
|
import { AtMenuConfigService } from './services';
|
||||||
@@ -19,6 +21,7 @@ export function configAtMenuConfigModule(framework: Framework) {
|
|||||||
DocDisplayMetaService,
|
DocDisplayMetaService,
|
||||||
WorkspaceDialogService,
|
WorkspaceDialogService,
|
||||||
RecentDocsService,
|
RecentDocsService,
|
||||||
WorkspaceDialogService,
|
EditorSettingService,
|
||||||
|
DocsService,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
|
import { fuzzyMatch } from '@affine/core/utils/fuzzy-match';
|
||||||
import { I18n, i18nTime } from '@affine/i18n';
|
import { I18n, i18nTime } from '@affine/i18n';
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
import {
|
import {
|
||||||
type AffineInlineEditor,
|
type AffineInlineEditor,
|
||||||
|
type DocMode,
|
||||||
type LinkedMenuGroup,
|
type LinkedMenuGroup,
|
||||||
type LinkedMenuItem,
|
type LinkedMenuItem,
|
||||||
type LinkedWidgetConfig,
|
type LinkedWidgetConfig,
|
||||||
LinkedWidgetUtils,
|
LinkedWidgetUtils,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
|
import { Text } from '@blocksuite/affine/store';
|
||||||
import type { EditorHost } from '@blocksuite/block-std';
|
import type { EditorHost } from '@blocksuite/block-std';
|
||||||
import { DateTimeIcon } from '@blocksuite/icons/lit';
|
import {
|
||||||
|
DateTimeIcon,
|
||||||
|
NewXxxEdgelessIcon,
|
||||||
|
NewXxxPageIcon,
|
||||||
|
} from '@blocksuite/icons/lit';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
import { signal } from '@preact/signals-core';
|
import { signal } from '@preact/signals-core';
|
||||||
import type { WorkspaceService } from '@toeverything/infra';
|
import type { DocsService, WorkspaceService } from '@toeverything/infra';
|
||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
import type { WorkspaceDialogService } from '../../dialogs';
|
import type { WorkspaceDialogService } from '../../dialogs';
|
||||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||||
import { JOURNAL_DATE_FORMAT, type JournalService } from '../../journal';
|
import type { EditorSettingService } from '../../editor-setting';
|
||||||
|
import { type JournalService, suggestJournalDate } from '../../journal';
|
||||||
import type { RecentDocsService } from '../../quicksearch';
|
import type { RecentDocsService } from '../../quicksearch';
|
||||||
|
|
||||||
const MAX_DOCS = 3;
|
const MAX_DOCS = 3;
|
||||||
@@ -31,7 +38,8 @@ export class AtMenuConfigService extends Service {
|
|||||||
private readonly docDisplayMetaService: DocDisplayMetaService,
|
private readonly docDisplayMetaService: DocDisplayMetaService,
|
||||||
private readonly dialogService: WorkspaceDialogService,
|
private readonly dialogService: WorkspaceDialogService,
|
||||||
private readonly recentDocsService: RecentDocsService,
|
private readonly recentDocsService: RecentDocsService,
|
||||||
private readonly workspaceDialogService: WorkspaceDialogService
|
private readonly editorSettingService: EditorSettingService,
|
||||||
|
private readonly docsService: DocsService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -64,26 +72,17 @@ export class AtMenuConfigService extends Service {
|
|||||||
!!this.journalService.journalDate$(d.id).value;
|
!!this.journalService.journalDate$(d.id).value;
|
||||||
const docItems = signal<LinkedMenuItem[]>([]);
|
const docItems = signal<LinkedMenuItem[]>([]);
|
||||||
|
|
||||||
|
const showRecent = query.trim().length === 0;
|
||||||
|
|
||||||
// recent docs should be at the top
|
// recent docs should be at the top
|
||||||
const recentDocs = this.recentDocsService.getRecentDocs();
|
const docMetas = showRecent
|
||||||
|
? this.recentDocsService
|
||||||
const sortedRawMetas =
|
.getRecentDocs()
|
||||||
query.trim().length === 0
|
.map(record => {
|
||||||
? rawMetas.toSorted((a, b) => {
|
return rawMetas.find(meta => meta.id === record.id);
|
||||||
const indexA = recentDocs.findIndex(doc => doc.id === a.id);
|
|
||||||
const indexB = recentDocs.findIndex(doc => doc.id === b.id);
|
|
||||||
|
|
||||||
if (indexA > -1 && indexB < 0) {
|
|
||||||
return -1;
|
|
||||||
} else if (indexA < 0 && indexB > -1) {
|
|
||||||
return 1;
|
|
||||||
} else if (indexA > -1 && indexB > -1) {
|
|
||||||
return indexA - indexB;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Number.MAX_SAFE_INTEGER;
|
|
||||||
})
|
})
|
||||||
: rawMetas;
|
.filter((m): m is DocMeta => !!m)
|
||||||
|
: rawMetas;
|
||||||
|
|
||||||
const docDisplayMetaService = this.docDisplayMetaService;
|
const docDisplayMetaService = this.docDisplayMetaService;
|
||||||
|
|
||||||
@@ -126,7 +125,7 @@ export class AtMenuConfigService extends Service {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const [index, meta] of sortedRawMetas.entries()) {
|
for (const [index, meta] of docMetas.entries()) {
|
||||||
if (abortSignal.aborted) {
|
if (abortSignal.aborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,7 +143,11 @@ export class AtMenuConfigService extends Service {
|
|||||||
})().catch(console.error);
|
})().catch(console.error);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: I18n.t('com.affine.editor.at-menu.link-to-doc'),
|
name: showRecent
|
||||||
|
? I18n.t('com.affine.editor.at-menu.recent-docs')
|
||||||
|
: I18n.t('com.affine.editor.at-menu.link-to-doc', {
|
||||||
|
query,
|
||||||
|
}),
|
||||||
items: docItems,
|
items: docItems,
|
||||||
maxDisplay: MAX_DOCS,
|
maxDisplay: MAX_DOCS,
|
||||||
get overflowText() {
|
get overflowText() {
|
||||||
@@ -182,13 +185,50 @@ export class AtMenuConfigService extends Service {
|
|||||||
return originalNewDocMenuGroup;
|
return originalNewDocMenuGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customNewDocItem: LinkedMenuItem = {
|
const createPage = (mode: DocMode) => {
|
||||||
...newDocItem,
|
const page = this.docsService.createDoc({
|
||||||
name: I18n.t('com.affine.editor.at-menu.create-doc', {
|
docProps: {
|
||||||
name: query || I18n.t('Untitled'),
|
note: this.editorSettingService.editorSetting.get('affine:note'),
|
||||||
}),
|
page: { title: new Text(query) },
|
||||||
|
},
|
||||||
|
primaryMode: mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
return page;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const customNewDocItems: LinkedMenuItem[] = [
|
||||||
|
{
|
||||||
|
key: 'create-page',
|
||||||
|
icon: NewXxxPageIcon(),
|
||||||
|
name: I18n.t('com.affine.editor.at-menu.create-page', {
|
||||||
|
name: query || I18n.t('Untitled'),
|
||||||
|
}),
|
||||||
|
action: () => {
|
||||||
|
close();
|
||||||
|
const page = createPage('page');
|
||||||
|
this.insertDoc(inlineEditor, page.id);
|
||||||
|
track.doc.editor.atMenu.createDoc({
|
||||||
|
mode: 'page',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'create-edgeless',
|
||||||
|
icon: NewXxxEdgelessIcon(),
|
||||||
|
name: I18n.t('com.affine.editor.at-menu.create-edgeless', {
|
||||||
|
name: query || I18n.t('Untitled'),
|
||||||
|
}),
|
||||||
|
action: () => {
|
||||||
|
close();
|
||||||
|
const page = createPage('edgeless');
|
||||||
|
this.insertDoc(inlineEditor, page.id);
|
||||||
|
track.doc.editor.atMenu.createDoc({
|
||||||
|
mode: 'edgeless',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
const customImportItem: LinkedMenuItem = {
|
const customImportItem: LinkedMenuItem = {
|
||||||
...importItem,
|
...importItem,
|
||||||
name: I18n.t('com.affine.editor.at-menu.import'),
|
name: I18n.t('com.affine.editor.at-menu.import'),
|
||||||
@@ -203,10 +243,7 @@ export class AtMenuConfigService extends Service {
|
|||||||
// If the imported file is a workspace file, insert the entry page node.
|
// If the imported file is a workspace file, insert the entry page node.
|
||||||
const { docIds, entryId, isWorkspaceFile } = payload;
|
const { docIds, entryId, isWorkspaceFile } = payload;
|
||||||
if (isWorkspaceFile && entryId) {
|
if (isWorkspaceFile && entryId) {
|
||||||
LinkedWidgetUtils.insertLinkedNode({
|
this.insertDoc(inlineEditor, entryId);
|
||||||
inlineEditor,
|
|
||||||
docId: entryId,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +258,7 @@ export class AtMenuConfigService extends Service {
|
|||||||
return {
|
return {
|
||||||
...originalNewDocMenuGroup,
|
...originalNewDocMenuGroup,
|
||||||
name: I18n.t('com.affine.editor.at-menu.new-doc'),
|
name: I18n.t('com.affine.editor.at-menu.new-doc'),
|
||||||
items: [customNewDocItem, customImportItem],
|
items: [...customNewDocItems, customImportItem],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,12 +289,12 @@ export class AtMenuConfigService extends Service {
|
|||||||
|
|
||||||
const { x, y, width, height } = getRect();
|
const { x, y, width, height } = getRect();
|
||||||
|
|
||||||
const id = this.workspaceDialogService.open('date-selector', {
|
const id = this.dialogService.open('date-selector', {
|
||||||
position: [x, y, width, height || 20],
|
position: [x, y, width, height || 20],
|
||||||
onSelect: date => {
|
onSelect: date => {
|
||||||
if (date) {
|
if (date) {
|
||||||
onSelectDate(date);
|
onSelectDate(date);
|
||||||
this.workspaceDialogService.close(id);
|
this.dialogService.close(id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -326,163 +363,3 @@ export class AtMenuConfigService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the name is a fuzzy match of the query.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const name = 'John Smith';
|
|
||||||
* const query = 'js';
|
|
||||||
* const isMatch = fuzzyMatch(name, query);
|
|
||||||
* // isMatch: true
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* if initialMatch = true, the first char must match as well
|
|
||||||
*/
|
|
||||||
function fuzzyMatch(name: string, query: string, matchInitial?: boolean) {
|
|
||||||
const pureName = name
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.split('')
|
|
||||||
.filter(char => char !== ' ')
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
const regex = new RegExp(
|
|
||||||
query
|
|
||||||
.split('')
|
|
||||||
.filter(char => char !== ' ')
|
|
||||||
.map(item => `${escapeRegExp(item)}.*`)
|
|
||||||
.join(''),
|
|
||||||
'i'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchInitial && query.length > 0 && !pureName.startsWith(query[0])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return regex.test(pureName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeRegExp(input: string) {
|
|
||||||
// escape regex characters in the input string to prevent regex format errors
|
|
||||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: infer locale from user's locale?
|
|
||||||
const monthNames = Array.from({ length: 12 }, (_, index) =>
|
|
||||||
new Intl.DateTimeFormat('en-US', { month: 'long' }).format(
|
|
||||||
new Date(2024, index)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// todo: infer locale from user's locale?
|
|
||||||
const weekDayNames = Array.from({ length: 7 }, (_, index) =>
|
|
||||||
new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(
|
|
||||||
new Date(2024, 0, index)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export function suggestJournalDate(query: string): {
|
|
||||||
dateString: string;
|
|
||||||
alias?: string;
|
|
||||||
} | null {
|
|
||||||
// given a query string, suggest a journal date
|
|
||||||
// if the query is empty or, starts with "t" AND matches today
|
|
||||||
// -> suggest today's date
|
|
||||||
// if the query starts with "y" AND matches "yesterday"
|
|
||||||
// -> suggest yesterday's date
|
|
||||||
// if the query starts with "l" AND matches last
|
|
||||||
// -> suggest last week's date
|
|
||||||
// if the query starts with "n" AND matches "next"
|
|
||||||
// -> suggest next week's date
|
|
||||||
// if the query starts with the first letter of a month and matches the month name
|
|
||||||
// -> if the trailing part matches a number
|
|
||||||
// -> suggest the date of the month
|
|
||||||
// -> otherwise, suggest the current day of the month
|
|
||||||
// otherwise, return null
|
|
||||||
query = query.trim().toLowerCase().split(' ').join('');
|
|
||||||
|
|
||||||
if (query === '' || fuzzyMatch('today', query, true)) {
|
|
||||||
return {
|
|
||||||
dateString: dayjs().format(JOURNAL_DATE_FORMAT),
|
|
||||||
alias: I18n.t('com.affine.today'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fuzzyMatch('tomorrow', query, true)) {
|
|
||||||
return {
|
|
||||||
dateString: dayjs().add(1, 'day').format(JOURNAL_DATE_FORMAT),
|
|
||||||
alias: I18n.t('com.affine.tomorrow'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fuzzyMatch('yesterday', query, true)) {
|
|
||||||
return {
|
|
||||||
dateString: dayjs().subtract(1, 'day').format(JOURNAL_DATE_FORMAT),
|
|
||||||
alias: I18n.t('com.affine.yesterday'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// next week dates, start from monday
|
|
||||||
const nextWeekDates = Array.from({ length: 7 }, (_, index) =>
|
|
||||||
dayjs()
|
|
||||||
.add(1, 'week')
|
|
||||||
.startOf('week')
|
|
||||||
.add(index, 'day')
|
|
||||||
.format(JOURNAL_DATE_FORMAT)
|
|
||||||
).map(date => ({
|
|
||||||
dateString: date,
|
|
||||||
alias: I18n.t('com.affine.next-week', {
|
|
||||||
weekday: weekDayNames[dayjs(date).day()],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const lastWeekDates = Array.from({ length: 7 }, (_, index) =>
|
|
||||||
dayjs()
|
|
||||||
.subtract(1, 'week')
|
|
||||||
.startOf('week')
|
|
||||||
.add(index, 'day')
|
|
||||||
.format(JOURNAL_DATE_FORMAT)
|
|
||||||
).map(date => ({
|
|
||||||
dateString: date,
|
|
||||||
alias: I18n.t('com.affine.last-week', {
|
|
||||||
weekday: weekDayNames[dayjs(date).day()],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const date of [...nextWeekDates, ...lastWeekDates]) {
|
|
||||||
const matched = fuzzyMatch(date.alias, query, true);
|
|
||||||
if (matched) {
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if query is a string that starts with alphabet letters and/or numbers
|
|
||||||
const regex = new RegExp(`^([a-z]+)(\\d*)$`, 'i');
|
|
||||||
const matched = query.match(regex);
|
|
||||||
|
|
||||||
if (matched) {
|
|
||||||
const [_, letters, numbers] = matched;
|
|
||||||
|
|
||||||
for (const month of monthNames) {
|
|
||||||
const monthMatched = fuzzyMatch(month, letters, true);
|
|
||||||
if (monthMatched) {
|
|
||||||
let day = numbers ? parseInt(numbers) : dayjs().date();
|
|
||||||
const invalidDay = day < 1 || day > 31;
|
|
||||||
if (invalidDay) {
|
|
||||||
// fallback to today's day
|
|
||||||
day = dayjs().date();
|
|
||||||
}
|
|
||||||
const year = dayjs().year();
|
|
||||||
return {
|
|
||||||
dateString: dayjs(`${year}-${month}-${day}`).format(
|
|
||||||
JOURNAL_DATE_FORMAT
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import { I18n } from '@affine/i18n';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import { suggestJournalDate } from '../../services/index';
|
import { suggestJournalDate } from '../suggest-journal-date';
|
||||||
|
|
||||||
describe('suggestJournalDate', () => {
|
describe('suggestJournalDate', () => {
|
||||||
test('today', () => {
|
test('today', () => {
|
||||||
@@ -17,6 +17,7 @@ export {
|
|||||||
type MaybeDate,
|
type MaybeDate,
|
||||||
} from './services/journal';
|
} from './services/journal';
|
||||||
export { JournalDocService } from './services/journal-doc';
|
export { JournalDocService } from './services/journal-doc';
|
||||||
|
export { suggestJournalDate } from './suggest-journal-date';
|
||||||
|
|
||||||
export function configureJournalModule(framework: Framework) {
|
export function configureJournalModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { fuzzyMatch } from '@affine/core/utils/fuzzy-match';
|
||||||
|
import { I18n } from '@affine/i18n';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { JOURNAL_DATE_FORMAT } from './services/journal';
|
||||||
|
|
||||||
|
// todo: infer locale from user's locale?
|
||||||
|
const monthNames = Array.from({ length: 12 }, (_, index) =>
|
||||||
|
new Intl.DateTimeFormat('en-US', { month: 'long' }).format(
|
||||||
|
new Date(2024, index)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// todo: infer locale from user's locale?
|
||||||
|
const weekDayNames = Array.from({ length: 7 }, (_, index) =>
|
||||||
|
new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(
|
||||||
|
new Date(2024, 0, index)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function suggestJournalDate(query: string): {
|
||||||
|
dateString: string;
|
||||||
|
alias?: string;
|
||||||
|
} | null {
|
||||||
|
// given a query string, suggest a journal date
|
||||||
|
// if the query is empty or, starts with "t" AND matches today
|
||||||
|
// -> suggest today's date
|
||||||
|
// if the query starts with "y" AND matches "yesterday"
|
||||||
|
// -> suggest yesterday's date
|
||||||
|
// if the query starts with "l" AND matches last
|
||||||
|
// -> suggest last week's date
|
||||||
|
// if the query starts with "n" AND matches "next"
|
||||||
|
// -> suggest next week's date
|
||||||
|
// if the query starts with the first letter of a month and matches the month name
|
||||||
|
// -> if the trailing part matches a number
|
||||||
|
// -> suggest the date of the month
|
||||||
|
// -> otherwise, suggest the current day of the month
|
||||||
|
// otherwise, return null
|
||||||
|
query = query.trim().toLowerCase().split(' ').join('');
|
||||||
|
|
||||||
|
if (query === '' || fuzzyMatch('today', query, true)) {
|
||||||
|
return {
|
||||||
|
dateString: dayjs().format(JOURNAL_DATE_FORMAT),
|
||||||
|
alias: I18n.t('com.affine.today'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fuzzyMatch('tomorrow', query, true)) {
|
||||||
|
return {
|
||||||
|
dateString: dayjs().add(1, 'day').format(JOURNAL_DATE_FORMAT),
|
||||||
|
alias: I18n.t('com.affine.tomorrow'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fuzzyMatch('yesterday', query, true)) {
|
||||||
|
return {
|
||||||
|
dateString: dayjs().subtract(1, 'day').format(JOURNAL_DATE_FORMAT),
|
||||||
|
alias: I18n.t('com.affine.yesterday'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// next week dates, start from monday
|
||||||
|
const nextWeekDates = Array.from({ length: 7 }, (_, index) =>
|
||||||
|
dayjs()
|
||||||
|
.add(1, 'week')
|
||||||
|
.startOf('week')
|
||||||
|
.add(index, 'day')
|
||||||
|
.format(JOURNAL_DATE_FORMAT)
|
||||||
|
).map(date => ({
|
||||||
|
dateString: date,
|
||||||
|
alias: I18n.t('com.affine.next-week', {
|
||||||
|
weekday: weekDayNames[dayjs(date).day()],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const lastWeekDates = Array.from({ length: 7 }, (_, index) =>
|
||||||
|
dayjs()
|
||||||
|
.subtract(1, 'week')
|
||||||
|
.startOf('week')
|
||||||
|
.add(index, 'day')
|
||||||
|
.format(JOURNAL_DATE_FORMAT)
|
||||||
|
).map(date => ({
|
||||||
|
dateString: date,
|
||||||
|
alias: I18n.t('com.affine.last-week', {
|
||||||
|
weekday: weekDayNames[dayjs(date).day()],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const date of [...nextWeekDates, ...lastWeekDates]) {
|
||||||
|
const matched = fuzzyMatch(date.alias, query, true);
|
||||||
|
if (matched) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if query is a string that starts with alphabet letters and/or numbers
|
||||||
|
const regex = new RegExp(`^([a-z]+)(\\d*)$`, 'i');
|
||||||
|
const matched = query.match(regex);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
const [_, letters, numbers] = matched;
|
||||||
|
|
||||||
|
for (const month of monthNames) {
|
||||||
|
const monthMatched = fuzzyMatch(month, letters, true);
|
||||||
|
if (monthMatched) {
|
||||||
|
let day = numbers ? parseInt(numbers) : dayjs().date();
|
||||||
|
const invalidDay = day < 1 || day > 31;
|
||||||
|
if (invalidDay) {
|
||||||
|
// fallback to today's day
|
||||||
|
day = dayjs().date();
|
||||||
|
}
|
||||||
|
const year = dayjs().year();
|
||||||
|
return {
|
||||||
|
dateString: dayjs(`${year}-${month}-${day}`).format(
|
||||||
|
JOURNAL_DATE_FORMAT
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
import { NewXxxEdgelessIcon, NewXxxPageIcon } from '@blocksuite/icons/rc';
|
||||||
import { Entity, LiveData } from '@toeverything/infra';
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
import type { QuickSearchSession } from '../providers/quick-search-provider';
|
||||||
@@ -34,7 +34,7 @@ export class CreationQuickSearchSession
|
|||||||
options: { keyWord: query },
|
options: { keyWord: query },
|
||||||
},
|
},
|
||||||
group,
|
group,
|
||||||
icon: PageIcon,
|
icon: NewXxxPageIcon,
|
||||||
payload: { mode: 'edgeless', title: query },
|
payload: { mode: 'edgeless', title: query },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ export class CreationQuickSearchSession
|
|||||||
options: { keyWord: query },
|
options: { keyWord: query },
|
||||||
},
|
},
|
||||||
group,
|
group,
|
||||||
icon: EdgelessIcon,
|
icon: NewXxxEdgelessIcon,
|
||||||
payload: { mode: 'edgeless', title: query },
|
payload: { mode: 'edgeless', title: query },
|
||||||
},
|
},
|
||||||
] as QuickSearchItem<'creation', { title: string; mode: DocMode }>[];
|
] as QuickSearchItem<'creation', { title: string; mode: DocMode }>[];
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Checks if the name is a fuzzy match of the query.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const name = 'John Smith';
|
||||||
|
* const query = 'js';
|
||||||
|
* const isMatch = fuzzyMatch(name, query);
|
||||||
|
* // isMatch: true
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* if initialMatch = true, the first char must match as well
|
||||||
|
*/
|
||||||
|
export function fuzzyMatch(
|
||||||
|
name: string,
|
||||||
|
query: string,
|
||||||
|
matchInitial?: boolean
|
||||||
|
) {
|
||||||
|
const pureName = name
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split('')
|
||||||
|
.filter(char => char !== ' ')
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const regex = new RegExp(
|
||||||
|
query
|
||||||
|
.split('')
|
||||||
|
.filter(char => char !== ' ')
|
||||||
|
.map(item => `${escapeRegExp(item)}.*`)
|
||||||
|
.join(''),
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchInitial && query.length > 0 && !pureName.startsWith(query[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return regex.test(pureName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(input: string) {
|
||||||
|
// escape regex characters in the input string to prevent regex format errors
|
||||||
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
@@ -193,7 +193,6 @@ export interface CreateCheckoutSessionInput {
|
|||||||
coupon?: InputMaybe<Scalars['String']['input']>;
|
coupon?: InputMaybe<Scalars['String']['input']>;
|
||||||
idempotencyKey?: InputMaybe<Scalars['String']['input']>;
|
idempotencyKey?: InputMaybe<Scalars['String']['input']>;
|
||||||
plan?: InputMaybe<SubscriptionPlan>;
|
plan?: InputMaybe<SubscriptionPlan>;
|
||||||
quantity?: InputMaybe<Scalars['Int']['input']>;
|
|
||||||
recurring?: InputMaybe<SubscriptionRecurring>;
|
recurring?: InputMaybe<SubscriptionRecurring>;
|
||||||
successCallbackLink: Scalars['String']['input'];
|
successCallbackLink: Scalars['String']['input'];
|
||||||
variant?: InputMaybe<SubscriptionVariant>;
|
variant?: InputMaybe<SubscriptionVariant>;
|
||||||
@@ -1257,6 +1256,9 @@ export interface WorkspaceType {
|
|||||||
id: Scalars['ID']['output'];
|
id: Scalars['ID']['output'];
|
||||||
/** is current workspace initialized */
|
/** is current workspace initialized */
|
||||||
initialized: Scalars['Boolean']['output'];
|
initialized: Scalars['Boolean']['output'];
|
||||||
|
/** Get user invoice count */
|
||||||
|
invoiceCount: Scalars['Int']['output'];
|
||||||
|
invoices: Array<InvoiceType>;
|
||||||
/** member count of workspace */
|
/** member count of workspace */
|
||||||
memberCount: Scalars['Int']['output'];
|
memberCount: Scalars['Int']['output'];
|
||||||
/** Members of workspace */
|
/** Members of workspace */
|
||||||
@@ -1290,6 +1292,11 @@ export interface WorkspaceTypeHistoriesArgs {
|
|||||||
take?: InputMaybe<Scalars['Int']['input']>;
|
take?: InputMaybe<Scalars['Int']['input']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceTypeInvoicesArgs {
|
||||||
|
skip?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
take?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkspaceTypeMembersArgs {
|
export interface WorkspaceTypeMembersArgs {
|
||||||
skip?: InputMaybe<Scalars['Int']['input']>;
|
skip?: InputMaybe<Scalars['Int']['input']>;
|
||||||
take?: InputMaybe<Scalars['Int']['input']>;
|
take?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
|||||||
@@ -1492,9 +1492,11 @@
|
|||||||
"com.affine.attachment.preview.error.subtitle": "file type not supported.",
|
"com.affine.attachment.preview.error.subtitle": "file type not supported.",
|
||||||
"com.affine.pdf.page.render.error": "Failed to render page.",
|
"com.affine.pdf.page.render.error": "Failed to render page.",
|
||||||
"com.affine.editor.journal-conflict.title": "Duplicate Entries in Today's Journal",
|
"com.affine.editor.journal-conflict.title": "Duplicate Entries in Today's Journal",
|
||||||
"com.affine.editor.at-menu.link-to-doc": "Link to Doc",
|
"com.affine.editor.at-menu.link-to-doc": "Search for \"{{query}}\"",
|
||||||
"com.affine.editor.at-menu.new-doc": "New Doc",
|
"com.affine.editor.at-menu.recent-docs": "Recent",
|
||||||
"com.affine.editor.at-menu.create-doc": "Create \"{{name}}\" Doc",
|
"com.affine.editor.at-menu.new-doc": "New",
|
||||||
|
"com.affine.editor.at-menu.create-page": "New \"{{name}}\" page",
|
||||||
|
"com.affine.editor.at-menu.create-edgeless": "New \"{{name}}\" edgeless",
|
||||||
"com.affine.editor.at-menu.import": "Import",
|
"com.affine.editor.at-menu.import": "Import",
|
||||||
"com.affine.editor.at-menu.more-docs-hint": "{{count}} more docs",
|
"com.affine.editor.at-menu.more-docs-hint": "{{count}} more docs",
|
||||||
"com.affine.editor.at-menu.journal": "Journal",
|
"com.affine.editor.at-menu.journal": "Journal",
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ const PageEvents = {
|
|||||||
doc: {
|
doc: {
|
||||||
editor: {
|
editor: {
|
||||||
slashMenu: ['linkDoc', 'createDoc', 'bookmark'],
|
slashMenu: ['linkDoc', 'createDoc', 'bookmark'],
|
||||||
atMenu: ['linkDoc', 'import'],
|
atMenu: ['linkDoc', 'import', 'createDoc'],
|
||||||
quickSearch: ['createDoc'],
|
quickSearch: ['createDoc'],
|
||||||
formatToolbar: ['bold'],
|
formatToolbar: ['bold'],
|
||||||
pageRef: ['navigate'],
|
pageRef: ['navigate'],
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ export const createLinkedPage = async (page: Page, pageName?: string) => {
|
|||||||
await expect(linkedPagePopover).toBeVisible();
|
await expect(linkedPagePopover).toBeVisible();
|
||||||
await type(page, pageName || 'Untitled');
|
await type(page, pageName || 'Untitled');
|
||||||
|
|
||||||
await page.keyboard.press('ArrowUp');
|
await linkedPagePopover
|
||||||
await page.keyboard.press('ArrowUp');
|
.locator(`icon-button`)
|
||||||
await page.keyboard.press('Enter', { delay: 50 });
|
.filter({ hasText: `New "${pageName}" page` })
|
||||||
|
.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function clickPageMoreActions(page: Page) {
|
export async function clickPageMoreActions(page: Page) {
|
||||||
|
|||||||
Reference in New Issue
Block a user