-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Export accounts from workspace to csv and add items from drag-n-drop csv #184
Changes from 1 commit
d2f56db
602e706
7628338
1690391
e9d05ab
d994c22
75dacb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
import { createCSV, downloadCSV } from '@/lib/csv' | ||
import { getLocalStorageData, setLocalStorageData } from '@/lib/local-storage' | ||
import { pluralize } from '@/lib/util' | ||
import { buildBlueSkyAppUrl, chunkArray, pluralize } from '@/lib/util' | ||
import { useLabelerAgent } from '@/shell/ConfigurationContext' | ||
import { AppBskyActorProfile, ToolsOzoneModerationDefs } from '@atproto/api' | ||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | ||
import { useRef } from 'react' | ||
import { toast } from 'react-toastify' | ||
|
@@ -95,6 +98,77 @@ export const useWorkspaceEmptyMutation = () => { | |
return mutation | ||
} | ||
|
||
export const useWorkspaceExportMutation = () => { | ||
const labelerAgent = useLabelerAgent() | ||
return useMutation({ | ||
mutationFn: async (items: string[]) => { | ||
// Items are exported in groups so we can expect all items in the group to be of same type | ||
// For now, only support exporting accounts | ||
if (!items[0].startsWith('did:')) { | ||
toast.error(`Exporting is only enabled for accounts.`) | ||
return [] | ||
} | ||
|
||
const data: Record< | ||
string, | ||
ToolsOzoneModerationDefs.RepoViewDetail | null | ||
> = {} | ||
|
||
for (const itemChunk of chunkArray(items, 50)) { | ||
await Promise.all( | ||
itemChunk.map(async (did) => { | ||
try { | ||
const { data: repo } = | ||
await labelerAgent.tools.ozone.moderation.getRepo({ | ||
did, | ||
}) | ||
|
||
data[did] = repo | ||
} catch (error) { | ||
// For now we're just swallowing errors and exporting what I can | ||
console.error(error) | ||
data[did] = null | ||
} | ||
}), | ||
) | ||
} | ||
|
||
downloadCSV( | ||
createCSV({ | ||
headers: [ | ||
'did', | ||
'handle', | ||
'email', | ||
'ip', | ||
'name', | ||
'labels', | ||
'profile', | ||
], | ||
lines: Object.values(data).map((repo) => { | ||
if (!repo) return '' | ||
const profile = AppBskyActorProfile.isRecord(repo.relatedRecords[0]) | ||
? (repo.relatedRecords[0] as AppBskyActorProfile.Record) | ||
: null | ||
|
||
const line: string[] = [ | ||
repo.did, | ||
repo.handle, | ||
repo.email || 'Unknown', | ||
`${repo.ip || 'Unknown'}`, | ||
`${profile?.displayName || 'Unknown'}`, | ||
repo.labels?.map(({ val }) => val).join(', ') || 'None', | ||
buildBlueSkyAppUrl({ did: repo.did }), | ||
] | ||
return line.join(',') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Values containing commas or newlines will end up breaking the CSV format. If we add some escaping we should be able to address it, though: const escapeCSVValue = (value: string) => {
if (value.contains(',') || value.contains('"') || value.contains('\r') || value.contains('\n')) {
return `"${value.replaceAll('"', '""')}"`
}
return value
}
// ...
return line.map(escapeCSVValue).join(',') There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yh except for labels/tags list, we don't have anything that contain those characters though but will add to make it more future proof. |
||
}), | ||
}), | ||
) | ||
|
||
return data | ||
}, | ||
}) | ||
} | ||
|
||
const getList = (): string[] => { | ||
const list = getLocalStorageData<string>(WORKSPACE_LIST_KEY) | ||
if (!list) return [] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
export type CsvContent = { | ||
filename: string | ||
headerRow: string | ||
body: string | ||
} | ||
|
||
export function createCSV({ | ||
headers, | ||
lines, | ||
filename, | ||
lineDelimiter = '\n', | ||
}: { | ||
lines: string[] | ||
headers: string[] | ||
filename?: string | ||
lineDelimiter?: string | ||
}) { | ||
return { | ||
filename: (filename || Date.now().toString()) + '.csv', | ||
headerRow: headers.join(',') + lineDelimiter, // make your own csv head | ||
body: lines.join(lineDelimiter), | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The return value does not match the returned type. Is there a particular reason for that ? The data structure to represent the CSV here is a mix between encoded data (a multi line string of comma separated values, with the firt line being a header) and "parsed" data (internal data structure), Looks like we could benefit from defining and using a type interface (or even a class ?) for internal representation of CSVs: type CsvContent = {
headers: string[]
rows: [][]
lineDelimiter: "," | ":" | "\t"
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
not sure if I follow? the return type is expected to be |
||
|
||
export function downloadCSV(csv: CsvContent) { | ||
var csvContent = csv.headerRow + csv.body | ||
if (!csvContent.match(/^data:text\/csv/i)) { | ||
csvContent = 'data:text/csv;charset=utf-8,' + csvContent // use 'data:text/csv;charset=utf-8,\ufeff', if you consider using the excel | ||
} | ||
var data = encodeURI(csvContent) | ||
|
||
var link = document.createElement('a') | ||
link.href = data | ||
link.download = csv.filename | ||
|
||
document.body.appendChild(link) | ||
link.click() | ||
document.body.removeChild(link) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I almost wonder if an empty value could be more useful than a default value here. I suppose it depends how the CSV will be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I think our current use case is for mod folks to get these exports and review manually in which case, I feel like the explicit default values are more helpful.