feedtack 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, f as FeedtackDeviceMeta, g as FeedtackPageMeta, h as FeedtackViewportMeta, i as FeedtackPinTarget } from './theme-BdqpMipn.js';
2
- export { A as AncestorNode, j as FeedtackArchive, k as FeedtackBoundingRect, l as FeedtackPin, m as FeedtackScope, n as FeedtackSentiment, o as FeedtackTheme, p as FeedtackUser, S as SCHEMA_VERSION, t as themeToCSS } from './theme-BdqpMipn.js';
1
+ import { F as FeedtackAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, C as ContentAdapter, f as ContentEditAdapter, g as FieldFilter, h as FieldApprovalState, i as FieldApproval, j as FeedtackDeviceMeta, k as FeedtackPageMeta, l as FeedtackViewportMeta, m as FeedtackPinTarget } from './adapter-Cn59URIG.js';
2
+ export { A as AncestorNode, n as FeedtackArchive, o as FeedtackBoundingRect, p as FeedtackPin, q as FeedtackScope, r as FeedtackSentiment, s as FeedtackUser, t as FieldChange, u as FocusedFieldInfo, S as SCHEMA_VERSION, v as isContentAdapter, w as isContentEditAdapter, x as warnIfNotContentAdapter, y as warnIfNotContentEditAdapter } from './adapter-Cn59URIG.js';
3
+ import { F as FeedtackEngineOpts, a as FeedtackEngineState, b as FeedtackStateListener } from './types-CHrWe7xT.js';
4
+ export { c as FeedtackFlushEvent, d as FeedtackTheme, g as generateId, t as themeToCSS } from './types-CHrWe7xT.js';
3
5
 
4
6
  /** Development adapter — logs all operations to the browser console */
5
7
  declare class ConsoleAdapter implements FeedtackAdapter {
@@ -34,9 +36,13 @@ interface WebhookAdapterConfig {
34
36
  updateUrl?: string;
35
37
  /** Required: async function that returns persisted feedback items */
36
38
  loadFeedback: (filter?: FeedtackFilter) => Promise<FeedbackItem[]>;
39
+ /** Optional: async function that returns stored field approvals */
40
+ loadApprovals?: (filter?: FieldFilter) => Promise<FieldApprovalState[]>;
41
+ /** Optional: async function that returns all stored field values for hydration */
42
+ loadFields?: () => Promise<Record<string, string>>;
37
43
  }
38
44
  /** Production adapter — POSTs feedback as JSON to a webhook endpoint */
39
- declare class WebhookAdapter implements FeedtackAdapter {
45
+ declare class WebhookAdapter implements FeedtackAdapter, ContentAdapter, ContentEditAdapter {
40
46
  private config;
41
47
  constructor(config: WebhookAdapterConfig);
42
48
  private post;
@@ -45,8 +51,28 @@ declare class WebhookAdapter implements FeedtackAdapter {
45
51
  resolve(feedbackId: string, resolution: Omit<FeedtackResolution, 'feedbackId'>): Promise<void>;
46
52
  archive(feedbackId: string, userId: string): Promise<void>;
47
53
  loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
54
+ approve(fieldPath: string, approval: FieldApproval): Promise<void>;
55
+ revokeApproval(fieldPath: string, userId: string): Promise<void>;
56
+ loadApprovals(filter?: FieldFilter): Promise<FieldApprovalState[]>;
57
+ loadFields(): Promise<Record<string, string>>;
58
+ saveField(fieldPath: string, value: string): Promise<void>;
48
59
  }
49
60
 
61
+ interface ScannedField {
62
+ fieldPath: string;
63
+ element: Element;
64
+ content: string;
65
+ }
66
+ /**
67
+ * Scans the DOM for elements annotated with data-feedtack-field.
68
+ * @param root - Element to search within. Defaults to document.body.
69
+ */
70
+ declare function scanFields(root?: Element): ScannedField[];
71
+ /**
72
+ * Computes a 12-character truncated SHA-256 hash of a string using Web Crypto API.
73
+ */
74
+ declare function hashField(content: string): Promise<string>;
75
+
50
76
  declare function getViewportMeta(): FeedtackViewportMeta;
51
77
  declare function getPageMeta(): FeedtackPageMeta;
52
78
  declare function getDeviceMeta(): FeedtackDeviceMeta;
@@ -65,4 +91,49 @@ declare function getCSSSelector(element: Element): string;
65
91
  /** Capture DOM target metadata at the clicked element */
66
92
  declare function getTargetMeta(element: Element): FeedtackPinTarget;
67
93
 
68
- export { ConsoleAdapter, FeedbackItem, FeedtackAdapter, FeedtackDeviceMeta, FeedtackFilter, FeedtackPageMeta, FeedtackPayload, FeedtackPinTarget, FeedtackReply, FeedtackResolution, FeedtackViewportMeta, LocalStorageAdapter, type LocalStorageAdapterConfig, WebhookAdapter, type WebhookAdapterConfig, getCSSSelector, getDeviceMeta, getPageMeta, getPinCoords, getTargetMeta, getViewportMeta };
94
+ interface FlushController {
95
+ flushPath: (path: string) => void;
96
+ clearFlushed: (path: string) => void;
97
+ detach: () => void;
98
+ }
99
+
100
+ declare class FeedtackEngine {
101
+ private readonly opts;
102
+ private state;
103
+ private listeners;
104
+ private actionCtx;
105
+ private styleEl;
106
+ private rootEl;
107
+ private spaNav;
108
+ flushCtrl: FlushController | null;
109
+ private inputHandles;
110
+ constructor(opts: FeedtackEngineOpts);
111
+ getState(): Readonly<FeedtackEngineState>;
112
+ subscribe(listener: FeedtackStateListener): () => void;
113
+ private setState;
114
+ mount(): void;
115
+ destroy(): void;
116
+ private onNavUpdate;
117
+ activatePinMode(): void;
118
+ deactivatePinMode(): void;
119
+ setComment(v: string): void;
120
+ setSentiment(v: FeedtackEngineState['sentiment']): void;
121
+ setCommentError(v: boolean): void;
122
+ setSelectedColor(c: string): void;
123
+ setOpenThreadId(id: string | null): void;
124
+ setReplyBody(v: string): void;
125
+ setComposeScope(s: 'site' | 'page'): void;
126
+ openModal(): void;
127
+ closeModal(): void;
128
+ getCurrentScope(): "site" | "page" | "element";
129
+ isArchivedForUser(item: FeedbackItem): boolean;
130
+ hasUnread(item: FeedbackItem): boolean;
131
+ hasValidPins(item: FeedbackItem): boolean;
132
+ handleSubmit(): Promise<void>;
133
+ handleModalSubmit(): Promise<void>;
134
+ handleReply(id: string): Promise<void>;
135
+ handleResolve(id: string): Promise<void>;
136
+ handleArchive(id: string): Promise<void>;
137
+ }
138
+
139
+ export { ConsoleAdapter, ContentAdapter, ContentEditAdapter, FeedbackItem, FeedtackAdapter, FeedtackDeviceMeta, FeedtackEngine, FeedtackEngineOpts, FeedtackEngineState, FeedtackFilter, FeedtackPageMeta, FeedtackPayload, FeedtackPinTarget, FeedtackReply, FeedtackResolution, FeedtackStateListener, FeedtackViewportMeta, FieldApproval, FieldApprovalState, FieldFilter, LocalStorageAdapter, type LocalStorageAdapterConfig, WebhookAdapter, type WebhookAdapterConfig, getCSSSelector, getDeviceMeta, getPageMeta, getPinCoords, getTargetMeta, getViewportMeta, hashField, scanFields };
package/dist/index.js CHANGED
@@ -1,13 +1,21 @@
1
1
  import {
2
+ FeedtackEngine,
2
3
  SCHEMA_VERSION,
4
+ generateId,
3
5
  getCSSSelector,
4
6
  getDeviceMeta,
5
7
  getPageMeta,
6
8
  getPinCoords,
7
9
  getTargetMeta,
8
10
  getViewportMeta,
9
- themeToCSS
10
- } from "./chunk-2A5LLDLP.js";
11
+ hashField,
12
+ isContentAdapter,
13
+ isContentEditAdapter,
14
+ scanFields,
15
+ themeToCSS,
16
+ warnIfNotContentAdapter,
17
+ warnIfNotContentEditAdapter
18
+ } from "./chunk-GD2SY64K.js";
11
19
 
12
20
  // src/adapters/ConsoleAdapter.ts
13
21
  var ConsoleAdapter = class {
@@ -132,17 +140,56 @@ var WebhookAdapter = class {
132
140
  async loadFeedback(filter) {
133
141
  return this.config.loadFeedback(filter);
134
142
  }
143
+ // ContentAdapter implementation
144
+ async approve(fieldPath, approval) {
145
+ const url = this.config.updateUrl ?? this.config.submitUrl;
146
+ await this.post(url, { type: "approve", fieldPath, ...approval });
147
+ }
148
+ async revokeApproval(fieldPath, userId) {
149
+ const url = this.config.updateUrl ?? this.config.submitUrl;
150
+ await this.post(url, { type: "revoke", fieldPath, userId });
151
+ }
152
+ async loadApprovals(filter) {
153
+ if (this.config.loadApprovals) {
154
+ return this.config.loadApprovals(filter);
155
+ }
156
+ return [];
157
+ }
158
+ // ContentEditAdapter implementation
159
+ async loadFields() {
160
+ if (this.config.loadFields) {
161
+ return this.config.loadFields();
162
+ }
163
+ return {};
164
+ }
165
+ async saveField(fieldPath, value) {
166
+ const url = this.config.updateUrl ?? this.config.submitUrl;
167
+ await this.post(url, {
168
+ type: "save-field",
169
+ fieldPath,
170
+ value,
171
+ clearApproval: true
172
+ });
173
+ }
135
174
  };
136
175
  export {
137
176
  ConsoleAdapter,
177
+ FeedtackEngine,
138
178
  LocalStorageAdapter,
139
179
  SCHEMA_VERSION,
140
180
  WebhookAdapter,
181
+ generateId,
141
182
  getCSSSelector,
142
183
  getDeviceMeta,
143
184
  getPageMeta,
144
185
  getPinCoords,
145
186
  getTargetMeta,
146
187
  getViewportMeta,
147
- themeToCSS
188
+ hashField,
189
+ isContentAdapter,
190
+ isContentEditAdapter,
191
+ scanFields,
192
+ themeToCSS,
193
+ warnIfNotContentAdapter,
194
+ warnIfNotContentEditAdapter
148
195
  };
@@ -0,0 +1,27 @@
1
+ import { F as FeedtackAdapter, C as ContentAdapter, f as ContentEditAdapter, a as FeedtackPayload, b as FeedtackReply, c as FeedtackResolution, d as FeedtackFilter, e as FeedbackItem, i as FieldApproval, g as FieldFilter, h as FieldApprovalState } from '../adapter-Cn59URIG.js';
2
+
3
+ interface DiskAdapterConfig {
4
+ /** Directory to store JSON files in. Default: '.feedback' */
5
+ directory?: string;
6
+ }
7
+ /** Node.js adapter — persists each feedback item as a JSON file on disk */
8
+ declare class DiskAdapter implements FeedtackAdapter, ContentAdapter, ContentEditAdapter {
9
+ private dir;
10
+ private approvalsDir;
11
+ private fieldsDir;
12
+ constructor(config?: DiskAdapterConfig);
13
+ submit(payload: FeedtackPayload): Promise<void>;
14
+ reply(feedbackId: string, reply: Omit<FeedtackReply, 'id' | 'feedbackId'>): Promise<void>;
15
+ resolve(feedbackId: string, resolution: Omit<FeedtackResolution, 'feedbackId'>): Promise<void>;
16
+ archive(feedbackId: string, userId: string): Promise<void>;
17
+ loadFeedback(filter?: FeedtackFilter): Promise<FeedbackItem[]>;
18
+ approve(fieldPath: string, approval: FieldApproval): Promise<void>;
19
+ revokeApproval(fieldPath: string, userId: string): Promise<void>;
20
+ loadApprovals(filter?: FieldFilter): Promise<FieldApprovalState[]>;
21
+ loadFields(): Promise<Record<string, string>>;
22
+ saveField(fieldPath: string, value: string): Promise<void>;
23
+ private read;
24
+ private write;
25
+ }
26
+
27
+ export { DiskAdapter, type DiskAdapterConfig };
@@ -0,0 +1,169 @@
1
+ // src/adapters/DiskAdapter.ts
2
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ var DiskAdapter = class {
5
+ constructor(config = {}) {
6
+ this.dir = config.directory ?? ".feedback";
7
+ this.approvalsDir = join(this.dir, "approvals");
8
+ this.fieldsDir = join(this.dir, "fields");
9
+ }
10
+ async submit(payload) {
11
+ await mkdir(this.dir, { recursive: true });
12
+ const item = {
13
+ payload,
14
+ replies: [],
15
+ resolutions: [],
16
+ archives: []
17
+ };
18
+ await this.write(payload.id, item);
19
+ }
20
+ async reply(feedbackId, reply) {
21
+ const item = await this.read(feedbackId);
22
+ item.replies.push({
23
+ id: `r_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`,
24
+ feedbackId,
25
+ ...reply
26
+ });
27
+ await this.write(feedbackId, item);
28
+ }
29
+ async resolve(feedbackId, resolution) {
30
+ const item = await this.read(feedbackId);
31
+ item.resolutions.push({ feedbackId, ...resolution });
32
+ await this.write(feedbackId, item);
33
+ }
34
+ async archive(feedbackId, userId) {
35
+ const item = await this.read(feedbackId);
36
+ item.archives.push({
37
+ feedbackId,
38
+ archivedBy: { id: userId, name: "", role: "" },
39
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
40
+ });
41
+ await this.write(feedbackId, item);
42
+ }
43
+ async loadFeedback(filter) {
44
+ let files;
45
+ try {
46
+ await mkdir(this.dir, { recursive: true });
47
+ files = (await readdir(this.dir)).filter((f) => f.endsWith(".json"));
48
+ } catch {
49
+ return [];
50
+ }
51
+ const items = await Promise.all(
52
+ files.map(
53
+ async (f) => JSON.parse(
54
+ await readFile(join(this.dir, f), "utf-8")
55
+ )
56
+ )
57
+ );
58
+ if (!filter) return items;
59
+ return items.filter((item) => {
60
+ if (filter.scope && item.payload.scope !== filter.scope) return false;
61
+ if (filter.pathname && item.payload.page.pathname !== filter.pathname)
62
+ return false;
63
+ if (filter.url && item.payload.page.url !== filter.url) return false;
64
+ if (filter.userId && item.payload.submittedBy.id !== filter.userId)
65
+ return false;
66
+ return true;
67
+ });
68
+ }
69
+ // ContentAdapter implementation
70
+ async approve(fieldPath, approval) {
71
+ await mkdir(this.approvalsDir, { recursive: true });
72
+ const safeName = fieldPath.replace(/[^a-zA-Z0-9._-]/g, "_");
73
+ await writeFile(
74
+ join(this.approvalsDir, `${safeName}.json`),
75
+ JSON.stringify(approval, null, 2)
76
+ );
77
+ }
78
+ async revokeApproval(fieldPath, userId) {
79
+ await mkdir(this.approvalsDir, { recursive: true });
80
+ const safeName = fieldPath.replace(/[^a-zA-Z0-9._-]/g, "_");
81
+ const filePath = join(this.approvalsDir, `${safeName}.json`);
82
+ let approval;
83
+ try {
84
+ approval = JSON.parse(await readFile(filePath, "utf-8"));
85
+ } catch {
86
+ return;
87
+ }
88
+ approval.by = approval.by.filter((id) => id !== userId);
89
+ if (approval.by.length === 0) {
90
+ const { unlink } = await import("fs/promises");
91
+ await unlink(filePath).catch(() => {
92
+ });
93
+ } else {
94
+ await writeFile(filePath, JSON.stringify(approval, null, 2));
95
+ }
96
+ }
97
+ async loadApprovals(filter) {
98
+ let files;
99
+ try {
100
+ await mkdir(this.approvalsDir, { recursive: true });
101
+ files = (await readdir(this.approvalsDir)).filter(
102
+ (f) => f.endsWith(".json")
103
+ );
104
+ } catch {
105
+ return [];
106
+ }
107
+ const states = await Promise.all(
108
+ files.map(async (f) => {
109
+ const fieldPath = f.replace(/\.json$/, "").replace(/_/g, ".");
110
+ const approval = JSON.parse(
111
+ await readFile(join(this.approvalsDir, f), "utf-8")
112
+ );
113
+ return {
114
+ fieldPath,
115
+ approval,
116
+ stale: false
117
+ };
118
+ })
119
+ );
120
+ if (!filter) return states;
121
+ return states.filter((s) => {
122
+ if (filter.fieldPath && s.fieldPath !== filter.fieldPath) return false;
123
+ return true;
124
+ });
125
+ }
126
+ // ContentEditAdapter implementation
127
+ async loadFields() {
128
+ let files;
129
+ try {
130
+ await mkdir(this.fieldsDir, { recursive: true });
131
+ files = (await readdir(this.fieldsDir)).filter((f) => f.endsWith(".json"));
132
+ } catch {
133
+ return {};
134
+ }
135
+ const entries = await Promise.all(
136
+ files.map(async (f) => {
137
+ const fieldPath = f.replace(/\.json$/, "").replace(/_/g, ".");
138
+ const value = JSON.parse(
139
+ await readFile(join(this.fieldsDir, f), "utf-8")
140
+ );
141
+ return [fieldPath, value];
142
+ })
143
+ );
144
+ return Object.fromEntries(entries);
145
+ }
146
+ async saveField(fieldPath, value) {
147
+ const safeName = fieldPath.replace(/[^a-zA-Z0-9._-]/g, "_");
148
+ await mkdir(this.fieldsDir, { recursive: true });
149
+ await writeFile(
150
+ join(this.fieldsDir, `${safeName}.json`),
151
+ JSON.stringify(value, null, 2)
152
+ );
153
+ const approvalPath = join(this.approvalsDir, `${safeName}.json`);
154
+ const { unlink } = await import("fs/promises");
155
+ await unlink(approvalPath).catch(() => {
156
+ });
157
+ }
158
+ async read(id) {
159
+ return JSON.parse(
160
+ await readFile(join(this.dir, `${id}.json`), "utf-8")
161
+ );
162
+ }
163
+ async write(id, item) {
164
+ await writeFile(join(this.dir, `${id}.json`), JSON.stringify(item, null, 2));
165
+ }
166
+ };
167
+ export {
168
+ DiskAdapter
169
+ };
@@ -1,11 +1,49 @@
1
+ import { d as FeedtackTheme, c as FeedtackFlushEvent } from '../types-CHrWe7xT.js';
1
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { h as FieldApprovalState, F as FeedtackAdapter, u as FocusedFieldInfo, t as FieldChange, s as FeedtackUser, e as FeedbackItem } from '../adapter-Cn59URIG.js';
2
4
  import React from 'react';
3
- import { e as FeedbackItem, F as FeedtackAdapter, p as FeedtackUser, o as FeedtackTheme } from '../theme-BdqpMipn.js';
4
5
 
5
6
  /** Fixed palette of 6 colors for pin markers */
6
7
  declare const PIN_PALETTE: readonly ["#ef4444", "#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#ec4899"];
7
8
  type PinColor = (typeof PIN_PALETTE)[number];
8
9
 
10
+ interface DeployCheckResult {
11
+ approved: boolean;
12
+ pending: string[];
13
+ }
14
+ interface UseContentApprovalOptions {
15
+ /**
16
+ * When provided, hash computation uses stored values from this map instead of
17
+ * reading element.textContent from the DOM. Use when static-build values may
18
+ * differ from live stored values (e.g. when used alongside useContentEdit).
19
+ */
20
+ storedValues?: Map<string, string>;
21
+ }
22
+ interface UseContentApprovalResult {
23
+ fields: FieldApprovalState[];
24
+ approve: (fieldPath: string) => Promise<void>;
25
+ revoke: (fieldPath: string) => Promise<void>;
26
+ rescan: () => Promise<void>;
27
+ checkDeploy: () => Promise<DeployCheckResult>;
28
+ }
29
+ /**
30
+ * Hook for managing content field approvals.
31
+ * Requires an adapter that implements ContentAdapter.
32
+ */
33
+ declare function useContentApproval(adapter: FeedtackAdapter, userId: string, options?: UseContentApprovalOptions): UseContentApprovalResult;
34
+
35
+ interface ContentEditToolbarProps {
36
+ focusedField: FocusedFieldInfo | null;
37
+ approvalState: FieldApprovalState | null;
38
+ changes: FieldChange[];
39
+ saving: string | null;
40
+ onApprove: (fieldPath: string) => Promise<void>;
41
+ onRevoke: (fieldPath: string) => Promise<void>;
42
+ onRevert: (fieldPath: string) => Promise<void>;
43
+ onCheckDeploy: () => Promise<DeployCheckResult>;
44
+ }
45
+ declare function ContentEditToolbar({ focusedField, approvalState, changes, saving, onApprove, onRevoke, onRevert, onCheckDeploy, }: ContentEditToolbarProps): react_jsx_runtime.JSX.Element;
46
+
9
47
  interface FeedtackContextValue {
10
48
  activatePinMode: () => void;
11
49
  deactivatePinMode: () => void;
@@ -18,11 +56,6 @@ interface FeedtackContextValue {
18
56
  isModalOpen: boolean;
19
57
  }
20
58
 
21
- interface FeedtackFlushEvent {
22
- pathname: string;
23
- items: FeedbackItem[];
24
- }
25
-
26
59
  interface FeedtackClasses {
27
60
  button?: string;
28
61
  form?: string;
@@ -53,10 +86,31 @@ interface FeedtackProviderProps {
53
86
  flushIdleMs?: number;
54
87
  /** User roles that trigger re-scope on reply (default: any non-'agent' role) */
55
88
  rescopeRoles?: string[];
89
+ /**
90
+ * Called by the consumer (e.g. on a Deploy button click) to check whether all
91
+ * content fields have current approvals. Feedtack surfaces the data; the consumer
92
+ * decides what to do with the result.
93
+ */
94
+ onDeployCheck?: () => Promise<{
95
+ approved: boolean;
96
+ pending: string[];
97
+ }>;
56
98
  }
57
99
  declare function FeedtackProvider({ children, adapter, currentUser, hotkey, adminOnly, theme, classes, sentimentLabels, onError, disabled, renderPinIcon, onFlush, flushIdleMs, rescopeRoles, }: FeedtackProviderProps): react_jsx_runtime.JSX.Element;
58
100
 
101
+ interface UseContentEditResult extends UseContentApprovalResult {
102
+ active: boolean;
103
+ activate: () => Promise<void>;
104
+ deactivate: () => void;
105
+ changes: FieldChange[];
106
+ revert: (fieldPath: string) => Promise<void>;
107
+ saving: string | null;
108
+ focusedField: FocusedFieldInfo | null;
109
+ toolbarProps: ContentEditToolbarProps;
110
+ }
111
+ declare function useContentEdit(adapter: FeedtackAdapter, userId: string): UseContentEditResult;
112
+
59
113
  /** Hook for host app to programmatically control feedtack */
60
114
  declare function useFeedtack(): FeedtackContextValue;
61
115
 
62
- export { type FeedtackClasses, type FeedtackContextValue, type FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, PIN_PALETTE, type PinColor, useFeedtack };
116
+ export { ContentEditToolbar, type ContentEditToolbarProps, type DeployCheckResult, type FeedtackClasses, type FeedtackContextValue, FeedtackFlushEvent, FeedtackProvider, type FeedtackProviderProps, type FeedtackSentimentLabels, PIN_PALETTE, type PinColor, type UseContentApprovalOptions, type UseContentApprovalResult, type UseContentEditResult, useContentApproval, useContentEdit, useFeedtack };