tinacms 1.5.5 → 1.5.7

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.
@@ -1,4 +1,12 @@
1
1
  /// <reference types="react" />
2
+ import type { TinaCMS } from '@tinacms/toolkit';
2
3
  declare const CollectionCreatePage: () => JSX.Element;
3
- export declare const RenderForm: ({ cms, collection, folder, templateName, mutationInfo, customDefaults, }: any) => JSX.Element;
4
+ export declare const RenderForm: ({ cms, collection, folder, templateName, mutationInfo, customDefaults, }: {
5
+ cms: TinaCMS;
6
+ collection: any;
7
+ folder: any;
8
+ templateName: any;
9
+ mutationInfo: any;
10
+ customDefaults?: any;
11
+ }) => JSX.Element;
4
12
  export default CollectionCreatePage;
package/dist/index.es.js CHANGED
@@ -255,10 +255,8 @@ mutation addPendingDocumentMutation(
255
255
  }
256
256
  return json.data;
257
257
  }
258
- async syncTinaMedia() {
259
- const res = await this.fetchWithToken(`${this.contentApiBase}/assets/${this.clientId}/sync/${this.branch}`, { method: "POST" });
260
- const jsonRes = await res.json();
261
- return jsonRes;
258
+ get appDashboardLink() {
259
+ return `${this.frontendUrl}/projects/${this.clientId}`;
262
260
  }
263
261
  async checkSyncStatus({
264
262
  assetsSyncing
@@ -290,6 +288,12 @@ mutation addPendingDocumentMutation(
290
288
  }).join(""));
291
289
  return JSON.parse(jsonPayload);
292
290
  }
291
+ async getProject() {
292
+ const res = await this.fetchWithToken(`${this.identityApiUrl}/v2/apps/${this.clientId}`, {
293
+ method: "GET"
294
+ });
295
+ return res.json();
296
+ }
293
297
  async getRefreshedToken(tokens) {
294
298
  const { access_token, id_token, refresh_token } = JSON.parse(tokens);
295
299
  const { exp, iss, client_id } = this.parseJwt(access_token);
@@ -1962,6 +1966,9 @@ var styles = `.tina-tailwind {
1962
1966
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1963
1967
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1964
1968
  }
1969
+ .tina-tailwind .outline {
1970
+ outline-style: solid;
1971
+ }
1965
1972
  .tina-tailwind .ring-1 {
1966
1973
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1967
1974
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -3202,9 +3209,10 @@ const useCollectionFolder = () => {
3202
3209
  const loc = useLocation();
3203
3210
  useEffect(() => {
3204
3211
  const match = loc.pathname.match(folderRegex);
3212
+ const folderName = match ? decodeURIComponent(match[1]) : "";
3205
3213
  const update = {
3206
- name: match ? match[1] : "",
3207
- fullyQualifiedName: match ? match[1] ? `~/${match[1]}` : "~" : "",
3214
+ name: folderName,
3215
+ fullyQualifiedName: match ? folderName ? `~/${folderName}` : "~" : "",
3208
3216
  loading: false,
3209
3217
  parentName: ""
3210
3218
  };
@@ -4104,6 +4112,18 @@ const RenderForm$1 = ({
4104
4112
  const windowWidth = useWindowWidth();
4105
4113
  const renderNavToggle = windowWidth < navBreakpoint + 1;
4106
4114
  const headerPadding = renderNavToggle ? "px-20" : "px-6";
4115
+ React.useEffect(() => {
4116
+ cms.dispatch({ type: "forms:add", value: form });
4117
+ cms.dispatch({ type: "forms:set-active-form-id", value: form.id });
4118
+ return () => {
4119
+ cms.dispatch({ type: "forms:remove", value: form.id });
4120
+ cms.dispatch({ type: "forms:set-active-form-id", value: null });
4121
+ };
4122
+ }, [JSON.stringify(formInfo.fields)]);
4123
+ if (!cms.state.activeFormId) {
4124
+ return null;
4125
+ }
4126
+ const activeForm = cms.state.forms.find(({ tinaForm }) => tinaForm.id === form.id);
4107
4127
  return /* @__PURE__ */ React.createElement(PageWrapper, null, /* @__PURE__ */ React.createElement(React.Fragment, null, ((_f = (_e = cms == null ? void 0 : cms.api) == null ? void 0 : _e.tina) == null ? void 0 : _f.isLocalMode) ? /* @__PURE__ */ React.createElement(LocalWarning, null) : /* @__PURE__ */ React.createElement(BillingWarning, null), /* @__PURE__ */ React.createElement("div", {
4108
4128
  className: `py-4 border-b border-gray-200 bg-white ${headerPadding}`
4109
4129
  }, /* @__PURE__ */ React.createElement("div", {
@@ -4121,8 +4141,8 @@ const RenderForm$1 = ({
4121
4141
  className: "text-xl text-gray-700 font-medium leading-tight"
4122
4142
  }, "Create New")), /* @__PURE__ */ React.createElement(FormStatus, {
4123
4143
  pristine: formIsPristine
4124
- }))), /* @__PURE__ */ React.createElement(FormBuilder, {
4125
- form,
4144
+ }))), activeForm && /* @__PURE__ */ React.createElement(FormBuilder, {
4145
+ form: activeForm,
4126
4146
  onPristineChange: setFormIsPristine
4127
4147
  })));
4128
4148
  };
@@ -4284,6 +4304,18 @@ const RenderForm = ({
4284
4304
  const windowWidth = useWindowWidth();
4285
4305
  const renderNavToggle = windowWidth < navBreakpoint + 1;
4286
4306
  const headerPadding = renderNavToggle ? "px-20" : "px-6";
4307
+ React.useEffect(() => {
4308
+ cms.dispatch({ type: "forms:add", value: form });
4309
+ cms.dispatch({ type: "forms:set-active-form-id", value: form.id });
4310
+ return () => {
4311
+ cms.dispatch({ type: "forms:remove", value: form.id });
4312
+ cms.dispatch({ type: "forms:set-active-form-id", value: null });
4313
+ };
4314
+ }, [JSON.stringify(document._values)]);
4315
+ if (!cms.state.activeFormId) {
4316
+ return null;
4317
+ }
4318
+ const activeForm = cms.state.forms.find(({ tinaForm }) => tinaForm.id === form.id);
4287
4319
  return /* @__PURE__ */ React.createElement(React.Fragment, null, ((_b = (_a = cms == null ? void 0 : cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isLocalMode) ? /* @__PURE__ */ React.createElement(LocalWarning, null) : /* @__PURE__ */ React.createElement(BillingWarning, null), /* @__PURE__ */ React.createElement("div", {
4288
4320
  className: `py-4 border-b border-gray-200 bg-white ${headerPadding}`
4289
4321
  }, /* @__PURE__ */ React.createElement("div", {
@@ -4301,8 +4333,8 @@ const RenderForm = ({
4301
4333
  className: "text-xl text-gray-700 font-medium leading-tight"
4302
4334
  }, "Edit ", `${filename}.${collection.format}`)), /* @__PURE__ */ React.createElement(FormStatus, {
4303
4335
  pristine: formIsPristine
4304
- }))), /* @__PURE__ */ React.createElement(FormBuilder, {
4305
- form,
4336
+ }))), activeForm && /* @__PURE__ */ React.createElement(FormBuilder, {
4337
+ form: activeForm,
4306
4338
  onPristineChange: setFormIsPristine
4307
4339
  }));
4308
4340
  };
package/dist/index.js CHANGED
@@ -270,10 +270,8 @@ mutation addPendingDocumentMutation(
270
270
  }
271
271
  return json.data;
272
272
  }
273
- async syncTinaMedia() {
274
- const res = await this.fetchWithToken(`${this.contentApiBase}/assets/${this.clientId}/sync/${this.branch}`, { method: "POST" });
275
- const jsonRes = await res.json();
276
- return jsonRes;
273
+ get appDashboardLink() {
274
+ return `${this.frontendUrl}/projects/${this.clientId}`;
277
275
  }
278
276
  async checkSyncStatus({
279
277
  assetsSyncing
@@ -305,6 +303,12 @@ mutation addPendingDocumentMutation(
305
303
  }).join(""));
306
304
  return JSON.parse(jsonPayload);
307
305
  }
306
+ async getProject() {
307
+ const res = await this.fetchWithToken(`${this.identityApiUrl}/v2/apps/${this.clientId}`, {
308
+ method: "GET"
309
+ });
310
+ return res.json();
311
+ }
308
312
  async getRefreshedToken(tokens) {
309
313
  const { access_token, id_token, refresh_token } = JSON.parse(tokens);
310
314
  const { exp, iss, client_id } = this.parseJwt(access_token);
@@ -1977,6 +1981,9 @@ mutation addPendingDocumentMutation(
1977
1981
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1978
1982
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1979
1983
  }
1984
+ .tina-tailwind .outline {
1985
+ outline-style: solid;
1986
+ }
1980
1987
  .tina-tailwind .ring-1 {
1981
1988
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1982
1989
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -3217,9 +3224,10 @@ This will work when developing locally but NOT when deployed to production.
3217
3224
  const loc = reactRouterDom.useLocation();
3218
3225
  React.useEffect(() => {
3219
3226
  const match = loc.pathname.match(folderRegex);
3227
+ const folderName = match ? decodeURIComponent(match[1]) : "";
3220
3228
  const update = {
3221
- name: match ? match[1] : "",
3222
- fullyQualifiedName: match ? match[1] ? `~/${match[1]}` : "~" : "",
3229
+ name: folderName,
3230
+ fullyQualifiedName: match ? folderName ? `~/${folderName}` : "~" : "",
3223
3231
  loading: false,
3224
3232
  parentName: ""
3225
3233
  };
@@ -4119,6 +4127,18 @@ This will work when developing locally but NOT when deployed to production.
4119
4127
  const windowWidth = windowSize.useWindowWidth();
4120
4128
  const renderNavToggle = windowWidth < navBreakpoint + 1;
4121
4129
  const headerPadding = renderNavToggle ? "px-20" : "px-6";
4130
+ React__default["default"].useEffect(() => {
4131
+ cms.dispatch({ type: "forms:add", value: form });
4132
+ cms.dispatch({ type: "forms:set-active-form-id", value: form.id });
4133
+ return () => {
4134
+ cms.dispatch({ type: "forms:remove", value: form.id });
4135
+ cms.dispatch({ type: "forms:set-active-form-id", value: null });
4136
+ };
4137
+ }, [JSON.stringify(formInfo.fields)]);
4138
+ if (!cms.state.activeFormId) {
4139
+ return null;
4140
+ }
4141
+ const activeForm = cms.state.forms.find(({ tinaForm }) => tinaForm.id === form.id);
4122
4142
  return /* @__PURE__ */ React__default["default"].createElement(PageWrapper, null, /* @__PURE__ */ React__default["default"].createElement(React__default["default"].Fragment, null, ((_f = (_e = cms == null ? void 0 : cms.api) == null ? void 0 : _e.tina) == null ? void 0 : _f.isLocalMode) ? /* @__PURE__ */ React__default["default"].createElement(toolkit.LocalWarning, null) : /* @__PURE__ */ React__default["default"].createElement(toolkit.BillingWarning, null), /* @__PURE__ */ React__default["default"].createElement("div", {
4123
4143
  className: `py-4 border-b border-gray-200 bg-white ${headerPadding}`
4124
4144
  }, /* @__PURE__ */ React__default["default"].createElement("div", {
@@ -4136,8 +4156,8 @@ This will work when developing locally but NOT when deployed to production.
4136
4156
  className: "text-xl text-gray-700 font-medium leading-tight"
4137
4157
  }, "Create New")), /* @__PURE__ */ React__default["default"].createElement(toolkit.FormStatus, {
4138
4158
  pristine: formIsPristine
4139
- }))), /* @__PURE__ */ React__default["default"].createElement(toolkit.FormBuilder, {
4140
- form,
4159
+ }))), activeForm && /* @__PURE__ */ React__default["default"].createElement(toolkit.FormBuilder, {
4160
+ form: activeForm,
4141
4161
  onPristineChange: setFormIsPristine
4142
4162
  })));
4143
4163
  };
@@ -4299,6 +4319,18 @@ This will work when developing locally but NOT when deployed to production.
4299
4319
  const windowWidth = windowSize.useWindowWidth();
4300
4320
  const renderNavToggle = windowWidth < navBreakpoint + 1;
4301
4321
  const headerPadding = renderNavToggle ? "px-20" : "px-6";
4322
+ React__default["default"].useEffect(() => {
4323
+ cms.dispatch({ type: "forms:add", value: form });
4324
+ cms.dispatch({ type: "forms:set-active-form-id", value: form.id });
4325
+ return () => {
4326
+ cms.dispatch({ type: "forms:remove", value: form.id });
4327
+ cms.dispatch({ type: "forms:set-active-form-id", value: null });
4328
+ };
4329
+ }, [JSON.stringify(document._values)]);
4330
+ if (!cms.state.activeFormId) {
4331
+ return null;
4332
+ }
4333
+ const activeForm = cms.state.forms.find(({ tinaForm }) => tinaForm.id === form.id);
4302
4334
  return /* @__PURE__ */ React__default["default"].createElement(React__default["default"].Fragment, null, ((_b = (_a = cms == null ? void 0 : cms.api) == null ? void 0 : _a.tina) == null ? void 0 : _b.isLocalMode) ? /* @__PURE__ */ React__default["default"].createElement(toolkit.LocalWarning, null) : /* @__PURE__ */ React__default["default"].createElement(toolkit.BillingWarning, null), /* @__PURE__ */ React__default["default"].createElement("div", {
4303
4335
  className: `py-4 border-b border-gray-200 bg-white ${headerPadding}`
4304
4336
  }, /* @__PURE__ */ React__default["default"].createElement("div", {
@@ -4316,8 +4348,8 @@ This will work when developing locally but NOT when deployed to production.
4316
4348
  className: "text-xl text-gray-700 font-medium leading-tight"
4317
4349
  }, "Edit ", `${filename}.${collection.format}`)), /* @__PURE__ */ React__default["default"].createElement(toolkit.FormStatus, {
4318
4350
  pristine: formIsPristine
4319
- }))), /* @__PURE__ */ React__default["default"].createElement(toolkit.FormBuilder, {
4320
- form,
4351
+ }))), activeForm && /* @__PURE__ */ React__default["default"].createElement(toolkit.FormBuilder, {
4352
+ form: activeForm,
4321
4353
  onPristineChange: setFormIsPristine
4322
4354
  }));
4323
4355
  };
@@ -160,9 +160,7 @@ export declare class Client {
160
160
  request<ReturnType>(query: ((gqlTag: typeof gql) => DocumentNode) | string, { variables }: {
161
161
  variables: object;
162
162
  }): Promise<ReturnType>;
163
- syncTinaMedia(): Promise<{
164
- assetsSyncing: string[];
165
- }>;
163
+ get appDashboardLink(): string;
166
164
  checkSyncStatus({ assetsSyncing, }: {
167
165
  assetsSyncing: string[];
168
166
  }): Promise<{
@@ -179,6 +177,7 @@ export declare class Client {
179
177
  cursor?: string;
180
178
  }>;
181
179
  parseJwt(token: any): any;
180
+ getProject(): Promise<any>;
182
181
  getRefreshedToken(tokens: string): Promise<TokenObject>;
183
182
  isAuthorized(): Promise<boolean>;
184
183
  isAuthenticated(): Promise<boolean>;
package/dist/react.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- /**
2
- * This is an experimental version of the useTina hook,
3
- * it is only meant to be used with Tina in "iframe mode".
4
- */
5
1
  export declare function useTina<T extends object>(props: {
6
2
  query: string;
7
3
  variables: object;
@@ -19,9 +15,19 @@ export declare function useEditState(): {
19
15
  * is working with.
20
16
  */
21
17
  export declare const tinaField: <T extends object & {
22
- _tina_metadata?: {
23
- id: string;
24
- name?: string;
25
- fields: Record<string, string>;
18
+ _content_source?: {
19
+ queryId: string;
20
+ path: (number | string)[];
26
21
  };
27
- }>(obj: T, field?: Exclude<keyof T, "__typename" | "_sys">) => string;
22
+ }>(object: T, property?: Exclude<keyof T, "__typename" | "_sys">, index?: number) => string;
23
+ export declare const addMetadata: <T extends object>(id: string, object: T & {
24
+ type?: string;
25
+ _content_source?: unknown;
26
+ }, path: (string | number)[]) => T;
27
+ /**
28
+ * This is a pretty rudimentary approach to hashing the query and variables to
29
+ * ensure we treat multiple queries on the page uniquely. It's possible
30
+ * that we would have collisions, and I'm not sure of the likeliness but seems
31
+ * like it'd be rare.
32
+ */
33
+ export declare const hashFromQuery: (input: string) => string;
package/dist/react.es.js CHANGED
@@ -1,23 +1,110 @@
1
1
  import React from "react";
2
2
  function useTina(props) {
3
+ const stringifiedQuery = JSON.stringify({
4
+ query: props.query,
5
+ variables: props.variables
6
+ });
7
+ const id = React.useMemo(() => hashFromQuery(stringifiedQuery), [stringifiedQuery]);
3
8
  const [data, setData] = React.useState(props.data);
4
9
  const [isClient, setIsClient] = React.useState(false);
5
- const id = JSON.stringify({ query: props.query, variables: props.variables });
10
+ const [quickEditEnabled, setQuickEditEnabled] = React.useState(false);
11
+ const [isInTinaIframe, setIsInTinaIframe] = React.useState(false);
6
12
  React.useEffect(() => {
7
13
  setIsClient(true);
8
14
  setData(props.data);
9
15
  }, [id]);
16
+ React.useEffect(() => {
17
+ if (quickEditEnabled) {
18
+ let mouseDownHandler = function(e) {
19
+ const attributeNames = e.target.getAttributeNames();
20
+ const tinaAttribute = attributeNames.find((name) => name.startsWith("data-tina-field"));
21
+ let fieldName;
22
+ if (tinaAttribute) {
23
+ e.preventDefault();
24
+ e.stopPropagation();
25
+ fieldName = e.target.getAttribute(tinaAttribute);
26
+ } else {
27
+ const ancestor = e.target.closest("[data-tina-field], [data-tina-field-overlay]");
28
+ if (ancestor) {
29
+ const attributeNames2 = ancestor.getAttributeNames();
30
+ const tinaAttribute2 = attributeNames2.find((name) => name.startsWith("data-tina-field"));
31
+ if (tinaAttribute2) {
32
+ e.preventDefault();
33
+ e.stopPropagation();
34
+ fieldName = ancestor.getAttribute(tinaAttribute2);
35
+ }
36
+ }
37
+ }
38
+ if (fieldName) {
39
+ if (isInTinaIframe) {
40
+ parent.postMessage({ type: "field:selected", fieldName }, window.location.origin);
41
+ }
42
+ }
43
+ };
44
+ const style = document.createElement("style");
45
+ style.type = "text/css";
46
+ style.textContent = `
47
+ [data-tina-field] {
48
+ outline: 2px dashed rgba(34,150,254,0.5);
49
+ transition: box-shadow ease-out 150ms;
50
+ }
51
+ [data-tina-field]:hover {
52
+ box-shadow: inset 100vi 100vh rgba(34,150,254,0.3);
53
+ outline: 2px solid rgba(34,150,254,1);
54
+ cursor: pointer;
55
+ }
56
+ [data-tina-field-overlay] {
57
+ outline: 2px dashed rgba(34,150,254,0.5);
58
+ position: relative;
59
+ }
60
+ [data-tina-field-overlay]:hover {
61
+ cursor: pointer;
62
+ outline: 2px solid rgba(34,150,254,1);
63
+ }
64
+ [data-tina-field-overlay]::after {
65
+ content: '';
66
+ position: absolute;
67
+ inset: 0;
68
+ z-index: 20;
69
+ transition: opacity ease-out 150ms;
70
+ background-color: rgba(34,150,254,0.3);
71
+ opacity: 0;
72
+ }
73
+ [data-tina-field-overlay]:hover::after {
74
+ opacity: 1;
75
+ }
76
+ `;
77
+ document.head.appendChild(style);
78
+ document.body.classList.add("__tina-quick-editing-enabled");
79
+ document.addEventListener("click", mouseDownHandler, true);
80
+ return () => {
81
+ document.removeEventListener("click", mouseDownHandler, true);
82
+ document.body.classList.remove("__tina-quick-editing-enabled");
83
+ style.remove();
84
+ };
85
+ }
86
+ }, [quickEditEnabled, isInTinaIframe]);
10
87
  React.useEffect(() => {
11
88
  parent.postMessage({ type: "open", ...props, id }, window.location.origin);
12
89
  window.addEventListener("message", (event) => {
90
+ if (event.data.type === "quickEditEnabled") {
91
+ setQuickEditEnabled(event.data.value);
92
+ }
13
93
  if (event.data.id === id && event.data.type === "updateData") {
14
94
  setData(event.data.data);
95
+ setIsInTinaIframe(true);
96
+ const anyTinaField = document.querySelector("[data-tina-field]");
97
+ if (anyTinaField) {
98
+ parent.postMessage({ type: "quick-edit", value: true }, window.location.origin);
99
+ } else {
100
+ parent.postMessage({ type: "quick-edit", value: false }, window.location.origin);
101
+ }
15
102
  }
16
103
  });
17
104
  return () => {
18
105
  parent.postMessage({ type: "close", id }, window.location.origin);
19
106
  };
20
- }, [id]);
107
+ }, [id, setQuickEditEnabled]);
21
108
  return { data, isClient };
22
109
  }
23
110
  function useEditState() {
@@ -35,16 +122,86 @@ function useEditState() {
35
122
  }, []);
36
123
  return { edit };
37
124
  }
38
- const tinaField = (obj, field) => {
125
+ const tinaField = (object, property, index) => {
39
126
  var _a, _b, _c;
40
- if (!field) {
41
- return `${(_a = obj._tina_metadata) == null ? void 0 : _a.id}#${(_b = obj._tina_metadata) == null ? void 0 : _b.name}`;
42
- }
43
- if (obj == null ? void 0 : obj._tina_metadata) {
44
- if (typeof field === "string") {
45
- return `${(_c = obj._tina_metadata) == null ? void 0 : _c.id}#${obj._tina_metadata.fields[field]}`;
127
+ if (object._content_source) {
128
+ if (!property) {
129
+ return [
130
+ (_a = object._content_source) == null ? void 0 : _a.queryId,
131
+ object._content_source.path.join(".")
132
+ ].join("---");
133
+ }
134
+ if (typeof index === "number") {
135
+ return [
136
+ (_b = object._content_source) == null ? void 0 : _b.queryId,
137
+ [...object._content_source.path, property, index].join(".")
138
+ ].join("---");
46
139
  }
140
+ return [
141
+ (_c = object._content_source) == null ? void 0 : _c.queryId,
142
+ [...object._content_source.path, property].join(".")
143
+ ].join("---");
47
144
  }
48
145
  return "";
49
146
  };
50
- export { tinaField, useEditState, useTina };
147
+ const addMetadata = (id, object, path) => {
148
+ Object.entries(object).forEach(([key, value]) => {
149
+ if (Array.isArray(value)) {
150
+ value.forEach((item, index) => {
151
+ if (isScalarOrUndefined(item)) {
152
+ return;
153
+ }
154
+ if (Array.isArray(item)) {
155
+ return;
156
+ }
157
+ const itemObject = item;
158
+ addMetadata(id, itemObject, [...path, key, index]);
159
+ });
160
+ } else {
161
+ if (isScalarOrUndefined(value)) {
162
+ return;
163
+ }
164
+ const itemObject = value;
165
+ addMetadata(id, itemObject, [...path, key]);
166
+ }
167
+ });
168
+ if ((object == null ? void 0 : object.type) === "root") {
169
+ return;
170
+ }
171
+ object._content_source = {
172
+ queryId: id,
173
+ path
174
+ };
175
+ return object;
176
+ };
177
+ function isScalarOrUndefined(value) {
178
+ const type = typeof value;
179
+ if (type === "string")
180
+ return true;
181
+ if (type === "number")
182
+ return true;
183
+ if (type === "boolean")
184
+ return true;
185
+ if (type === "undefined")
186
+ return true;
187
+ if (value == null)
188
+ return true;
189
+ if (value instanceof String)
190
+ return true;
191
+ if (value instanceof Number)
192
+ return true;
193
+ if (value instanceof Boolean)
194
+ return true;
195
+ return false;
196
+ }
197
+ const hashFromQuery = (input) => {
198
+ let hash = 0;
199
+ for (let i = 0; i < input.length; i++) {
200
+ const char = input.charCodeAt(i);
201
+ hash = (hash << 5) - hash + char & 4294967295;
202
+ }
203
+ const nonNegativeHash = Math.abs(hash);
204
+ const alphanumericHash = nonNegativeHash.toString(36);
205
+ return alphanumericHash;
206
+ };
207
+ export { addMetadata, hashFromQuery, tinaField, useEditState, useTina };
package/dist/react.js CHANGED
@@ -7,24 +7,111 @@
7
7
  }
8
8
  var React__default = /* @__PURE__ */ _interopDefaultLegacy(React);
9
9
  function useTina(props) {
10
+ const stringifiedQuery = JSON.stringify({
11
+ query: props.query,
12
+ variables: props.variables
13
+ });
14
+ const id = React__default["default"].useMemo(() => hashFromQuery(stringifiedQuery), [stringifiedQuery]);
10
15
  const [data, setData] = React__default["default"].useState(props.data);
11
16
  const [isClient, setIsClient] = React__default["default"].useState(false);
12
- const id = JSON.stringify({ query: props.query, variables: props.variables });
17
+ const [quickEditEnabled, setQuickEditEnabled] = React__default["default"].useState(false);
18
+ const [isInTinaIframe, setIsInTinaIframe] = React__default["default"].useState(false);
13
19
  React__default["default"].useEffect(() => {
14
20
  setIsClient(true);
15
21
  setData(props.data);
16
22
  }, [id]);
23
+ React__default["default"].useEffect(() => {
24
+ if (quickEditEnabled) {
25
+ let mouseDownHandler = function(e) {
26
+ const attributeNames = e.target.getAttributeNames();
27
+ const tinaAttribute = attributeNames.find((name) => name.startsWith("data-tina-field"));
28
+ let fieldName;
29
+ if (tinaAttribute) {
30
+ e.preventDefault();
31
+ e.stopPropagation();
32
+ fieldName = e.target.getAttribute(tinaAttribute);
33
+ } else {
34
+ const ancestor = e.target.closest("[data-tina-field], [data-tina-field-overlay]");
35
+ if (ancestor) {
36
+ const attributeNames2 = ancestor.getAttributeNames();
37
+ const tinaAttribute2 = attributeNames2.find((name) => name.startsWith("data-tina-field"));
38
+ if (tinaAttribute2) {
39
+ e.preventDefault();
40
+ e.stopPropagation();
41
+ fieldName = ancestor.getAttribute(tinaAttribute2);
42
+ }
43
+ }
44
+ }
45
+ if (fieldName) {
46
+ if (isInTinaIframe) {
47
+ parent.postMessage({ type: "field:selected", fieldName }, window.location.origin);
48
+ }
49
+ }
50
+ };
51
+ const style = document.createElement("style");
52
+ style.type = "text/css";
53
+ style.textContent = `
54
+ [data-tina-field] {
55
+ outline: 2px dashed rgba(34,150,254,0.5);
56
+ transition: box-shadow ease-out 150ms;
57
+ }
58
+ [data-tina-field]:hover {
59
+ box-shadow: inset 100vi 100vh rgba(34,150,254,0.3);
60
+ outline: 2px solid rgba(34,150,254,1);
61
+ cursor: pointer;
62
+ }
63
+ [data-tina-field-overlay] {
64
+ outline: 2px dashed rgba(34,150,254,0.5);
65
+ position: relative;
66
+ }
67
+ [data-tina-field-overlay]:hover {
68
+ cursor: pointer;
69
+ outline: 2px solid rgba(34,150,254,1);
70
+ }
71
+ [data-tina-field-overlay]::after {
72
+ content: '';
73
+ position: absolute;
74
+ inset: 0;
75
+ z-index: 20;
76
+ transition: opacity ease-out 150ms;
77
+ background-color: rgba(34,150,254,0.3);
78
+ opacity: 0;
79
+ }
80
+ [data-tina-field-overlay]:hover::after {
81
+ opacity: 1;
82
+ }
83
+ `;
84
+ document.head.appendChild(style);
85
+ document.body.classList.add("__tina-quick-editing-enabled");
86
+ document.addEventListener("click", mouseDownHandler, true);
87
+ return () => {
88
+ document.removeEventListener("click", mouseDownHandler, true);
89
+ document.body.classList.remove("__tina-quick-editing-enabled");
90
+ style.remove();
91
+ };
92
+ }
93
+ }, [quickEditEnabled, isInTinaIframe]);
17
94
  React__default["default"].useEffect(() => {
18
95
  parent.postMessage({ type: "open", ...props, id }, window.location.origin);
19
96
  window.addEventListener("message", (event) => {
97
+ if (event.data.type === "quickEditEnabled") {
98
+ setQuickEditEnabled(event.data.value);
99
+ }
20
100
  if (event.data.id === id && event.data.type === "updateData") {
21
101
  setData(event.data.data);
102
+ setIsInTinaIframe(true);
103
+ const anyTinaField = document.querySelector("[data-tina-field]");
104
+ if (anyTinaField) {
105
+ parent.postMessage({ type: "quick-edit", value: true }, window.location.origin);
106
+ } else {
107
+ parent.postMessage({ type: "quick-edit", value: false }, window.location.origin);
108
+ }
22
109
  }
23
110
  });
24
111
  return () => {
25
112
  parent.postMessage({ type: "close", id }, window.location.origin);
26
113
  };
27
- }, [id]);
114
+ }, [id, setQuickEditEnabled]);
28
115
  return { data, isClient };
29
116
  }
30
117
  function useEditState() {
@@ -42,18 +129,90 @@
42
129
  }, []);
43
130
  return { edit };
44
131
  }
45
- const tinaField = (obj, field) => {
132
+ const tinaField = (object, property, index) => {
46
133
  var _a, _b, _c;
47
- if (!field) {
48
- return `${(_a = obj._tina_metadata) == null ? void 0 : _a.id}#${(_b = obj._tina_metadata) == null ? void 0 : _b.name}`;
49
- }
50
- if (obj == null ? void 0 : obj._tina_metadata) {
51
- if (typeof field === "string") {
52
- return `${(_c = obj._tina_metadata) == null ? void 0 : _c.id}#${obj._tina_metadata.fields[field]}`;
134
+ if (object._content_source) {
135
+ if (!property) {
136
+ return [
137
+ (_a = object._content_source) == null ? void 0 : _a.queryId,
138
+ object._content_source.path.join(".")
139
+ ].join("---");
140
+ }
141
+ if (typeof index === "number") {
142
+ return [
143
+ (_b = object._content_source) == null ? void 0 : _b.queryId,
144
+ [...object._content_source.path, property, index].join(".")
145
+ ].join("---");
53
146
  }
147
+ return [
148
+ (_c = object._content_source) == null ? void 0 : _c.queryId,
149
+ [...object._content_source.path, property].join(".")
150
+ ].join("---");
54
151
  }
55
152
  return "";
56
153
  };
154
+ const addMetadata = (id, object, path) => {
155
+ Object.entries(object).forEach(([key, value]) => {
156
+ if (Array.isArray(value)) {
157
+ value.forEach((item, index) => {
158
+ if (isScalarOrUndefined(item)) {
159
+ return;
160
+ }
161
+ if (Array.isArray(item)) {
162
+ return;
163
+ }
164
+ const itemObject = item;
165
+ addMetadata(id, itemObject, [...path, key, index]);
166
+ });
167
+ } else {
168
+ if (isScalarOrUndefined(value)) {
169
+ return;
170
+ }
171
+ const itemObject = value;
172
+ addMetadata(id, itemObject, [...path, key]);
173
+ }
174
+ });
175
+ if ((object == null ? void 0 : object.type) === "root") {
176
+ return;
177
+ }
178
+ object._content_source = {
179
+ queryId: id,
180
+ path
181
+ };
182
+ return object;
183
+ };
184
+ function isScalarOrUndefined(value) {
185
+ const type = typeof value;
186
+ if (type === "string")
187
+ return true;
188
+ if (type === "number")
189
+ return true;
190
+ if (type === "boolean")
191
+ return true;
192
+ if (type === "undefined")
193
+ return true;
194
+ if (value == null)
195
+ return true;
196
+ if (value instanceof String)
197
+ return true;
198
+ if (value instanceof Number)
199
+ return true;
200
+ if (value instanceof Boolean)
201
+ return true;
202
+ return false;
203
+ }
204
+ const hashFromQuery = (input) => {
205
+ let hash = 0;
206
+ for (let i = 0; i < input.length; i++) {
207
+ const char = input.charCodeAt(i);
208
+ hash = (hash << 5) - hash + char & 4294967295;
209
+ }
210
+ const nonNegativeHash = Math.abs(hash);
211
+ const alphanumericHash = nonNegativeHash.toString(36);
212
+ return alphanumericHash;
213
+ };
214
+ exports2.addMetadata = addMetadata;
215
+ exports2.hashFromQuery = hashFromQuery;
57
216
  exports2.tinaField = tinaField;
58
217
  exports2.useEditState = useEditState;
59
218
  exports2.useTina = useTina;
package/dist/style.css CHANGED
@@ -991,6 +991,9 @@
991
991
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
992
992
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
993
993
  }
994
+ .tina-tailwind .outline {
995
+ outline-style: solid;
996
+ }
994
997
  .tina-tailwind .ring-1 {
995
998
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
996
999
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinacms",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "main": "dist/index.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "exports": {
@@ -58,7 +58,7 @@
58
58
  "@react-hook/window-size": "^3.0.7",
59
59
  "@tinacms/schema-tools": "1.4.4",
60
60
  "@tinacms/sharedctx": "1.0.1",
61
- "@tinacms/toolkit": "1.7.1",
61
+ "@tinacms/toolkit": "1.7.3",
62
62
  "crypto-js": "^4.0.0",
63
63
  "encoding": "0.1.13",
64
64
  "fetch-ponyfill": "^7.1.0",
@@ -68,7 +68,7 @@
68
68
  "lodash.set": "^4.3.2",
69
69
  "prism-react-renderer": "^1.3.5",
70
70
  "react-icons": "^4.3.1",
71
- "react-router-dom": "6",
71
+ "react-router-dom": "6.3.0",
72
72
  "yup": "^0.32.0",
73
73
  "zod": "^3.14.3"
74
74
  },