= {
+ image: 'Images',
+ pdf: 'PDFs',
+ document: 'Documents',
+ spreadsheet: 'Spreadsheets',
+ presentation: 'Presentations',
+ archive: 'Zip archives',
+ text: 'Text files',
+};
diff --git a/app/frontend/src/pages/TidyPage.tsx b/app/frontend/src/pages/TidyPage.tsx
index aa019c3..55ff344 100644
--- a/app/frontend/src/pages/TidyPage.tsx
+++ b/app/frontend/src/pages/TidyPage.tsx
@@ -4,7 +4,7 @@ import { EmptyState, ListSkeleton, PageHeader, Tabs, timeAgo } from '@project-gr
import { useAuth } from '../auth';
import { Choice, Suggestion } from '../components/TidyList';
import { batchSummary, itemsText, TidyPreview, undoSummary, type TidyAction, type TidyBatch, type TidyRequest } from '../components/TidyPreview';
-import { fileSize } from '../files';
+import { fileSize, fileTypeNames } from '../files';
import { useSpaces } from '../spaces';
import { money, shortDate } from '../subscriptions';
import { useAction } from '../useAction';
@@ -257,7 +257,7 @@ function TidyHistory() {
{batchSummary(batch)}
{timeAgo(batch.createdAt)}
- {batch.rule && ` · by the rule “${batch.rule.titleContains}”`}
+ {batch.rule && ` · by the rule “${batch.rule.titleContains || fileTypeNames[batch.rule.fileType ?? '']}”`}
{batch.undoneAt
? ' · undone'
: batch.remaining < batch.itemCount && ` · ${itemsText(batch.itemCount - batch.remaining)} since deleted`}
diff --git a/app/frontend/src/pages/spaces/TidyRules.tsx b/app/frontend/src/pages/spaces/TidyRules.tsx
index 885259c..d9d17a6 100644
--- a/app/frontend/src/pages/spaces/TidyRules.tsx
+++ b/app/frontend/src/pages/spaces/TidyRules.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { ConfirmDialog, ListSkeleton, TagChip, TextField } from '@project-graphite/ui';
import { useAuth } from '../../auth';
import { batchSummary, TidyPreview, type TidyRequest } from '../../components/TidyPreview';
+import { fileTypeNames } from '../../files';
import type { Tag } from '../../spaces';
import { useAction } from '../../useAction';
import { useResource } from '../../useResource';
@@ -14,6 +15,7 @@ interface Rule {
spaceId: string;
titleContains: string;
kind: string | null;
+ fileType: string | null;
tag: Tag;
enabled: boolean;
createdAt: string;
@@ -29,6 +31,12 @@ const kindNames: Record = {
file: 'files',
};
+function ruleText(rule: Rule) {
+ const words = rule.titleContains && `“${rule.titleContains}” in the title`;
+ if (rule.fileType) return `${fileTypeNames[rule.fileType]}${words ? ` with ${words}` : ''}`;
+ return `Title contains “${rule.titleContains}”${rule.kind ? `, ${kindNames[rule.kind]} only` : ''}`;
+}
+
export function TidyRules({ tags }: { tags: Tag[] }) {
const auth = useAuth();
const space = useSpace();
@@ -45,7 +53,7 @@ export function TidyRules({ tags }: { tags: Tag[] }) {
Tagging rules
- Tag new items in this space by words in their title.
+ Tag new items in this space by words in their title, by file type, or both.
{rules.error ? (
@@ -59,7 +67,7 @@ export function TidyRules({ tags }: { tags: Tag[] }) {
- Title contains “{rule.titleContains}”{rule.kind && `, ${kindNames[rule.kind]} only`} →
+ {ruleText(rule)} →
{rule.enabled ? 'On' : 'Paused'}
@@ -118,8 +126,10 @@ export function TidyRules({ tags }: { tags: Tag[] }) {
onSubmit={(event) => {
event.preventDefault();
const target = event.currentTarget;
- if (!form.check(target, { titleContains: [required('Enter the words to look for.'), atMost(100, 'Use at most 100 characters.')] })) return;
const values = new FormData(target);
+ const fileType = values.get('fileType');
+ const limit = atMost(100, 'Use at most 100 characters.');
+ if (!form.check(target, { titleContains: fileType ? [limit] : [required('Enter words to look for, or choose a file type.'), limit] })) return;
void creating
.run(async () => {
const created = await auth.request(`/spaces/${space.id}/tidy-rules`, {
@@ -127,18 +137,32 @@ export function TidyRules({ tags }: { tags: Tag[] }) {
body: JSON.stringify({
titleContains: String(values.get('titleContains')).trim(),
tagId: values.get('tagId'),
- ...(values.get('kind') ? { kind: values.get('kind') } : {}),
+ ...(fileType ? { fileType } : values.get('kind') ? { kind: values.get('kind') } : {}),
}),
});
rules.mutate((current) => [...current, created]);
- return `New items with “${created.titleContains}” in the title will be tagged ${created.tag.name}.`;
+ const words = created.titleContains && `“${created.titleContains}” in the title`;
+ return created.fileType
+ ? `${fileTypeNames[created.fileType]} uploaded from now on${words ? ` with ${words}` : ''} will be tagged ${created.tag.name}.`
+ : `New items with ${words} will be tagged ${created.tag.name}.`;
}, 'Could not add the rule')
.then((added) => added && target.reset());
}}
>
New rule
-
+
+