118 | {rows.length > 0 ? (
119 |
120 | {rows.map((row, index) => (
121 |
127 | ))}
128 |
129 | ) : (
130 | // empty
131 |
132 | {t('KeyValueList.empty')}
133 |
134 | )}
135 |
136 | {/* loading */}
137 | {!scrollPositionRestored && (
138 |
139 | {t('KeyValueList.loading')}
140 |
141 | )}
142 |
143 | )
144 | }
145 |
--------------------------------------------------------------------------------
/src/ui/hooks/useNotion.ts:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next'
2 |
3 | import type {
4 | NotionFomula,
5 | NotionKeyValue,
6 | NotionPage,
7 | NotionRichText,
8 | NotionTitle,
9 | } from '@/types/common'
10 |
11 | export default function useNotion() {
12 | const { t } = useTranslation()
13 |
14 | async function fetchNotion(options: {
15 | proxyUrl: string
16 | integrationToken: string
17 | databaseId: string
18 | keyPropertyName: string
19 | valuePropertyName: string
20 | nextCursor?: string
21 | keyValuesArray: NotionKeyValue[]
22 | }) {
23 | console.log('fetchNotion', options)
24 |
25 | // proxyUrlから末尾のスラッシュを削除
26 | const proxyUrl = options.proxyUrl.replace(/\/$/, '')
27 |
28 | // パラメータを定義
29 | // 引数nextCursorがある場合は、start_cursorを設定
30 | const reqParams = {
31 | page_size: 100,
32 | start_cursor: options.nextCursor || undefined,
33 | }
34 |
35 | // データベースをfetchしてpageの配列を取得
36 | const res = await fetch(
37 | `${proxyUrl}/https://api.notion.com/v1/databases/${options.databaseId}/query`,
38 | {
39 | method: 'POST',
40 | headers: {
41 | Authorization: `Bearer ${options.integrationToken}`,
42 | 'Notion-Version': '2021-08-16',
43 | 'Content-Type': 'application/json',
44 | },
45 | body: JSON.stringify(reqParams),
46 | },
47 | ).catch(() => {
48 | throw new Error(t('notifications.Fetch.error.failedFetch'))
49 | })
50 | const resJson = await res.json()
51 | console.log(resJson)
52 | const pages = resJson.results as NotionPage[]
53 |
54 | if (!pages) {
55 | // pagesが無かったら処理中断
56 | throw new Error(t('notifications.Fetch.error.noPages'))
57 | }
58 |
59 | // pageごとに処理実行
60 | pages.forEach(row => {
61 | // keyPropertyNameと同じプロパティが無かったら処理中断
62 | if (!row.properties[options.keyPropertyName]) {
63 | throw new Error(t('notifications.Fetch.error.wrongKeyName'))
64 | }
65 |
66 | // valuePropertyNameと同じプロパティが無かったら処理中断
67 | if (!row.properties[options.valuePropertyName]) {
68 | throw new Error(t('notifications.Fetch.error.wrongValueName'))
69 | }
70 |
71 | // keyPropertyNameからpropertyを探す
72 | const keyProperty = row.properties[options.keyPropertyName]
73 | // keyのtypeがtitle, formula, textでない場合は処理中断
74 | if (
75 | keyProperty.type !== 'title' &&
76 | keyProperty.type !== 'rich_text' &&
77 | keyProperty.type !== 'formula'
78 | ) {
79 | throw new Error(t('notifications.Fetch.error.wrongKeyType'))
80 | }
81 | // propertyのtypeを判別してkeyを取得する
82 | const key = getPropertyValue(keyProperty)
83 |
84 | // valuePropertyNameからpropertyを探す
85 | const valueProperty = row.properties[options.valuePropertyName]
86 | // valueのtypeがtitle, formula, textでない場合は処理中断
87 | if (
88 | valueProperty.type !== 'title' &&
89 | valueProperty.type !== 'rich_text' &&
90 | valueProperty.type !== 'formula'
91 | ) {
92 | throw new Error(t('notifications.Fetch.error.wrongValueType'))
93 | }
94 | // propertyのtypeを判別してvalueを取得する
95 | const value = getPropertyValue(valueProperty)
96 |
97 | // keyValuesの配列にkeyとvalueを追加
98 | options.keyValuesArray.push({
99 | id: row.id,
100 | key,
101 | value,
102 | created_time: row.created_time,
103 | last_edited_time: row.last_edited_time,
104 | url: row.url,
105 | })
106 | })
107 |
108 | if (resJson.has_more) {
109 | // resのhas_moreフラグがtrueなら、nextCursorに値を入れて再度fetchNotion関数を実行
110 | // falseなら終了
111 | await fetchNotion({ ...options, nextCursor: resJson.next_cursor })
112 | } else {
113 | return
114 | }
115 | }
116 |
117 | function getPropertyValue(
118 | property: NotionTitle | NotionFomula | NotionRichText,
119 | ): string {
120 | let value: string
121 |
122 | if (property.type === 'title') {
123 | if (property.title.length) {
124 | value = property.title[0].plain_text
125 | } else {
126 | value = ''
127 | }
128 | } else if (property.type === 'rich_text') {
129 | if (property.rich_text.length) {
130 | value = property.rich_text[0].plain_text
131 | } else {
132 | value = ''
133 | }
134 | } else if (property.type === 'formula') {
135 | value = property.formula.string
136 | } else {
137 | value = ''
138 | }
139 |
140 | return value
141 | }
142 |
143 | return { fetchNotion }
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/applyValue.ts:
--------------------------------------------------------------------------------
1 | import { loadFontsAsync } from '@create-figma-plugin/utilities'
2 | import queryString, { type ParsedQuery } from 'query-string'
3 |
4 | import i18n from '@/i18n/main'
5 | import { getTextNodes } from '@/main/util'
6 |
7 | import type { NotionKeyValue, TargetTextRange } from '@/types/common'
8 |
9 | export default async function applyValue(
10 | keyValues: NotionKeyValue[],
11 | options: {
12 | targetTextRange: TargetTextRange
13 | includeComponents: boolean
14 | includeInstances: boolean
15 | },
16 | ) {
17 | console.log('applyValue', keyValues, options)
18 |
19 | // textNodeを取得
20 | const textNodes = await getTextNodes(options)
21 |
22 | console.log('textNodes', textNodes)
23 |
24 | // textNodeが1つもなかったら処理を終了
25 | if (textNodes.length === 0) {
26 | // 選択状態によってトーストを出し分け
27 | if (options.targetTextRange === 'selection') {
28 | figma.notify(i18n.t('notifications.main.noTextInSelection'))
29 | } else if (options.targetTextRange === 'currentPage') {
30 | figma.notify(i18n.t('notifications.main.noTextInCurrentPage'))
31 | } else if (options.targetTextRange === 'allPages') {
32 | figma.notify(i18n.t('notifications.main.noTextInAllPages'))
33 | }
34 |
35 | return
36 | }
37 |
38 | // matchedTextNodesを格納する配列を用意
39 | let matchedTextNodes: TextNode[] = []
40 |
41 | // textNodeの中からレイヤー名が#で始まるものだけを探してmatchedTextNodesに追加
42 | matchedTextNodes = textNodes.filter(textNode => {
43 | return textNode.name.startsWith('#')
44 | })
45 |
46 | console.log('matchedTextNodes', matchedTextNodes)
47 |
48 | // matchedTextNodesが空なら処理を終了
49 | if (matchedTextNodes.length === 0) {
50 | figma.notify(i18n.t('notifications.applyValue.noMatchingText'))
51 | return
52 | }
53 |
54 | // 事前にフォントをロード
55 | await loadFontsAsync(matchedTextNodes).catch((error: Error) => {
56 | const errorMessage = i18n.t('notifications.main.errorLoadFonts')
57 | figma.notify(errorMessage, { error: true })
58 | throw new Error(errorMessage)
59 | })
60 |
61 | // matchedTextNodesごとに処理を実行
62 | matchedTextNodes.forEach(textNode => {
63 | // クエリパラメータを取得する
64 | // ?から後ろの部分をクエリパラメータと見なす
65 | // 一部の文字がうまくパースされないので、パース前にエンコードする
66 | const originalParam = textNode.name.split('?')[1] as string | undefined
67 | let param: ParsedQuery