Skip to content

Commit

Permalink
✨ Add keyword search in workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
foysalit committed Dec 9, 2024
1 parent d994c22 commit 75dacb3
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 58 deletions.
89 changes: 75 additions & 14 deletions components/workspace/FilterSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from '@/common/buttons'
import { CheckIcon } from '@heroicons/react/24/outline'
import { Checkbox } from '@/common/forms'
import { Checkbox, FormLabel, Input } from '@/common/forms'
import { useState } from 'react'
import { WorkspaceListData } from './useWorkspaceListData'
import { ToolsOzoneModerationDefs } from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyActorProfile,
ToolsOzoneModerationDefs,
} from '@atproto/api'
import { getSubjectStatusFromItemData } from './utils'

const toggleItemCheck = (item: string, select: boolean = true) => {
Expand Down Expand Up @@ -47,6 +51,14 @@ const ContentFilterOptions = {
contentWithVideoEmbed: { label: 'Content with video embed' },
}

const matchKeyword = (keyword?: string, subject?: string) => {
if (!keyword || !subject) return false
const keywords = keyword.split('||')
return keywords.some((k) =>
subject.toLowerCase().includes(k.trim().toLowerCase()),
)
}

export const WorkspaceFilterSelector = ({
listData,
}: {
Expand All @@ -66,6 +78,7 @@ export const WorkspaceFilterSelector = ({
contentAuthorDeactivated: false,
contentWithImageEmbed: false,
contentWithVideoEmbed: false,
keyword: '',
})

const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -86,6 +99,13 @@ export const WorkspaceFilterSelector = ({
})
}

const unselectAll = () => {
if (!listData) return
Object.keys(listData).forEach((uri) => {
toggleItemCheck(uri, false)
})
}

const toggleFilteredItems = (select: boolean = true) => {
if (!listData) return
Object.entries(listData).forEach(([uri, item]) => {
Expand All @@ -104,7 +124,14 @@ export const WorkspaceFilterSelector = ({
(filters.accountEmailUnConfirmed &&
isRepo &&
!item.emailConfirmedAt) ||
(filters.accountDeactivated && isRepo && item.deactivatedAt)
(filters.accountDeactivated && isRepo && item.deactivatedAt) ||
(filters.keyword &&
isRepo &&
matchKeyword(
filters.keyword,
item.relatedRecords?.find(AppBskyActorProfile.isRecord)
?.description,
))
) {
toggleItemCheck(uri, select)
}
Expand All @@ -125,7 +152,10 @@ export const WorkspaceFilterSelector = ({
(filters.contentWithImageEmbed &&
subjectStatus?.tags?.includes('embed:image')) ||
(filters.contentWithVideoEmbed &&
subjectStatus?.tags?.includes('embed:video'))
subjectStatus?.tags?.includes('embed:video')) ||
(filters.keyword &&
isRecord &&
matchKeyword(filters.keyword, item.value?.['text']))
) {
toggleItemCheck(uri, select)
}
Expand Down Expand Up @@ -204,24 +234,46 @@ export const WorkspaceFilterSelector = ({
)}
</div>
</div>
<div className="mb-2">
<FormLabel
label="Keyword"
htmlFor="keyword"
className="flex-1"
>
<Input
type="text"
id="keyword"
name="keyword"
required
list="subject-suggestions"
placeholder="Keyword"
className="block w-full"
value={filters.keyword}
onChange={(ev) =>
setFilters({ ...filters, keyword: ev.target.value })
}
autoComplete="off"
/>
</FormLabel>
</div>
<p className="py-2 block max-w-lg text-gray-500 dark:text-gray-300 text-xs">
Note:{' '}
<i>
You can select or unselect all items that matches the above
configured filters. The configured filters work with OR
operator. <br />
So, if you select {'"Deactivated accounts"'} and
{'"Accounts in appealed state"'}, all accounts that are
either deactivated OR in appealed state will be selected
</i>
You can select or unselect all items that matches the above
configured filters. The configured filters work with OR
operator. <br />
So, if you select {'"Deactivated accounts"'} and
{'"Accounts in appealed state"'}, all accounts that are either
deactivated OR in appealed state will be selected. <br />
<br />
You can use {'||'} separator in the keyword filter to look for
multiple keywords in either {"user's"} profile bio or record
content
</p>
<div className="flex flex-row mt-2 gap-2">
<ActionButton
size="xs"
appearance="outlined"
onClick={() => {
toggleFilteredItems()
// close()
}}
>
<span className="text-xs">Select Filtered</span>
Expand All @@ -235,6 +287,15 @@ export const WorkspaceFilterSelector = ({
>
<span className="text-xs">Unselect Filtered</span>
</ActionButton>
<ActionButton
size="xs"
appearance="outlined"
onClick={() => {
unselectAll()
}}
>
<span className="text-xs">Unselect All</span>
</ActionButton>
<ActionButton
size="xs"
appearance="outlined"
Expand Down
75 changes: 31 additions & 44 deletions lib/csv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function createCSV({
headers: string[]
filename?: string
lineDelimiter?: string
}) {
}): CsvContent {
return {
filename: (filename || Date.now().toString()) + '.csv',
headerRow: headers.join(',') + lineDelimiter, // make your own csv head
Expand Down Expand Up @@ -54,71 +54,58 @@ export const processFileForWorkspaceImport = (
return new Promise((resolve, reject) => {
const fileType = file.type
const fileName = file.name.toLowerCase()
const reader = new FileReader()
reader.onload = () => {
try {
const content = reader.result as string
let values: string[] = []

if (fileType === 'application/json' || fileName.endsWith('.json')) {
const reader = new FileReader()
reader.onload = () => {
try {
const jsonData = JSON.parse(reader.result as string)
const values = extractFromJSON(jsonData)
if (values.length === 0) {
reject(new Error(`No 'did' or 'uri' found in ${file.name}`))
}
resolve(values)
} catch (error) {
reject(new Error(`Invalid JSON file: ${file.name}`))
if (fileType === 'application/json' || fileName.endsWith('.json')) {
const jsonData = JSON.parse(content)
values = extractFromJSON(jsonData)
} else if (fileType === 'text/csv' || fileName.endsWith('.csv')) {
values = extractFromCSV(content)
} else {
return reject(new Error(`Unsupported file type: ${file.name}`))
}
}
reader.onerror = () => reject(new Error(`Failed to read ${file.name}`))
reader.readAsText(file)
} else if (fileType === 'text/csv' || fileName.endsWith('.csv')) {
const reader = new FileReader()
reader.onload = () => {
try {
const csvData = reader.result as string
const values = extractFromCSV(csvData)
if (values.length === 0) {
reject(new Error(`No 'did' or 'uri' found in ${file.name}`))
}
resolve(values)
} catch (error) {
reject(new Error(`Invalid CSV file: ${file.name}`))

if (values.length === 0) {
return reject(new Error(`No 'did' or 'uri' found in ${file.name}`))
}

resolve(values)
} catch (error) {
reject(new Error(`Error parsing file content: ${file.name}`))
}
reader.onerror = () => reject(new Error(`Failed to read ${file.name}`))
reader.readAsText(file)
} else {
reject(new Error(`Unsupported file type: ${file.name}`))
}
reader.onerror = () => reject(new Error(`Failed to read ${file.name}`))
reader.readAsText(file)
})
}

const cleanCSVColumn = (col: string) => {
const trimmed = col.trim()
return trimmed.startsWith('"') && trimmed.endsWith('"')
? trimmed.slice(1, -1)
: trimmed
}

export const extractFromCSV = (data: string): string[] => {
const rows = data.split('\n').map((row) => row.trim())
const [header, ...content] = rows

if (!header) return []

// In case header names are quoted, we want to exclude those quotes before check
const headers = header.split(',').map((col) => {
const trimmed = col.trim()
return trimmed.startsWith('"') && trimmed.endsWith('"')
? trimmed.slice(1, -1)
: trimmed
})
const headers = header.split(',').map(cleanCSVColumn)
const didIndex = headers.indexOf('did')
const uriIndex = headers.indexOf('uri')

if (didIndex === -1 && uriIndex === -1) return []

return content
.map((row) => {
const columns = row.split(',').map((col) => {
const trimmed = col.trim()
return trimmed.startsWith('"') && trimmed.endsWith('"')
? trimmed.slice(1, -1)
: trimmed
})
const columns = row.split(',').map(cleanCSVColumn)
return columns[didIndex] || columns[uriIndex]
})
.filter(Boolean)
Expand Down

0 comments on commit 75dacb3

Please sign in to comment.