diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md
index 9cd27c390b29ea..babe2b12d1c96d 100644
--- a/docs/designers-developers/developers/data/data-core-editor.md
+++ b/docs/designers-developers/developers/data/data-core-editor.md
@@ -655,6 +655,19 @@ _Related_
- hasMultiSelection in core/block-editor store.
+# **hasNonPostEntityChanges**
+
+Returns true if there are unsaved edits for entities other than
+the editor's post, and false otherwise.
+
+_Parameters_
+
+- _state_ `Object`: Global application state.
+
+_Returns_
+
+- `boolean`: Whether there are edits or not.
+
# **hasSelectedBlock**
_Related_
diff --git a/docs/designers-developers/developers/data/data-core.md b/docs/designers-developers/developers/data/data-core.md
index 79f3f41991c08f..f5e06a7659e038 100644
--- a/docs/designers-developers/developers/data/data-core.md
+++ b/docs/designers-developers/developers/data/data-core.md
@@ -153,6 +153,21 @@ _Returns_
- `?Object`: Record.
+# **getEntityRecordChangesByRecord**
+
+Returns a map of objects with each edited
+raw entity record and its corresponding edits.
+
+The map is keyed by entity `kind => name => key => { rawRecord, edits }`.
+
+_Parameters_
+
+- _state_ `Object`: State tree.
+
+_Returns_
+
+- `null`: The map of edited records with their edits.
+
# **getEntityRecordEdits**
Returns the specified entity record's edits.
diff --git a/package-lock.json b/package-lock.json
index fee8be7cc525cd..e9d3e71fa5c1ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7948,6 +7948,7 @@
"@wordpress/viewport": "file:packages/viewport",
"@wordpress/wordcount": "file:packages/wordcount",
"classnames": "^2.2.5",
+ "equivalent-key-map": "^0.2.2",
"lodash": "^4.17.15",
"memize": "^1.0.5",
"react-autosize-textarea": "^3.0.2",
diff --git a/packages/core-data/README.md b/packages/core-data/README.md
index a2bb3ccbe6deeb..5b5cda9580b62f 100644
--- a/packages/core-data/README.md
+++ b/packages/core-data/README.md
@@ -366,6 +366,21 @@ _Returns_
- `?Object`: Record.
+# **getEntityRecordChangesByRecord**
+
+Returns a map of objects with each edited
+raw entity record and its corresponding edits.
+
+The map is keyed by entity `kind => name => key => { rawRecord, edits }`.
+
+_Parameters_
+
+- _state_ `Object`: State tree.
+
+_Returns_
+
+- `null`: The map of edited records with their edits.
+
# **getEntityRecordEdits**
Returns the specified entity record's edits.
diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js
index 1475ffd2562f8f..a4ea47ac8c745e 100644
--- a/packages/core-data/src/selectors.js
+++ b/packages/core-data/src/selectors.js
@@ -153,6 +153,54 @@ export function getEntityRecords( state, kind, name, query ) {
return getQueriedItems( queriedState, query );
}
+/**
+ * Returns a map of objects with each edited
+ * raw entity record and its corresponding edits.
+ *
+ * The map is keyed by entity `kind => name => key => { rawRecord, edits }`.
+ *
+ * @param {Object} state State tree.
+ *
+ * @return {{ [kind: string]: { [name: string]: { [key: string]: { rawRecord: Object, edits: Object } } } }} The map of edited records with their edits.
+ */
+export const getEntityRecordChangesByRecord = createSelector(
+ ( state ) => {
+ const {
+ entities: { data },
+ } = state;
+ return Object.keys( data ).reduce( ( acc, kind ) => {
+ Object.keys( data[ kind ] ).forEach( ( name ) => {
+ const editsKeys = Object.keys( data[ kind ][ name ].edits ).filter( ( editsKey ) =>
+ hasEditsForEntityRecord( state, kind, name, editsKey )
+ );
+ if ( editsKeys.length ) {
+ if ( ! acc[ kind ] ) {
+ acc[ kind ] = {};
+ }
+ if ( ! acc[ kind ][ name ] ) {
+ acc[ kind ][ name ] = {};
+ }
+ editsKeys.forEach(
+ ( editsKey ) =>
+ ( acc[ kind ][ name ][ editsKey ] = {
+ rawRecord: getRawEntityRecord( state, kind, name, editsKey ),
+ edits: getEntityRecordNonTransientEdits(
+ state,
+ kind,
+ name,
+ editsKey
+ ),
+ } )
+ );
+ }
+ } );
+
+ return acc;
+ }, {} );
+ },
+ ( state ) => [ state.entities.data ]
+);
+
/**
* Returns the specified entity record's edits.
*
diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js
index 13e1551f2f1d6e..ae75557d40f1a9 100644
--- a/packages/core-data/src/test/selectors.js
+++ b/packages/core-data/src/test/selectors.js
@@ -9,6 +9,7 @@ import deepFreeze from 'deep-freeze';
import {
getEntityRecord,
getEntityRecords,
+ getEntityRecordChangesByRecord,
getEntityRecordNonTransientEdits,
getEmbedPreview,
isPreviewEmbedFallback,
@@ -105,10 +106,63 @@ describe( 'getEntityRecords', () => {
} );
} );
+describe( 'getEntityRecordChangesByRecord', () => {
+ it( 'should return a map of objects with each raw edited entity record and its corresponding edits', () => {
+ const state = deepFreeze( {
+ entities: {
+ config: [
+ {
+ kind: 'someKind',
+ name: 'someName',
+ transientEdits: { someTransientEditProperty: true },
+ },
+ ],
+ data: {
+ someKind: {
+ someName: {
+ queriedData: {
+ items: {
+ someKey: {
+ someProperty: 'somePersistedValue',
+ someRawProperty: { raw: 'somePersistedRawValue' },
+ },
+ },
+ },
+ edits: {
+ someKey: {
+ someProperty: 'someEditedValue',
+ someRawProperty: 'someEditedRawValue',
+ someTransientEditProperty: 'someEditedTransientEditValue',
+ },
+ },
+ },
+ },
+ },
+ },
+ } );
+ expect( getEntityRecordChangesByRecord( state ) ).toEqual( {
+ someKind: {
+ someName: {
+ someKey: {
+ rawRecord: {
+ someProperty: 'somePersistedValue',
+ someRawProperty: 'somePersistedRawValue',
+ },
+ edits: {
+ someProperty: 'someEditedValue',
+ someRawProperty: 'someEditedRawValue',
+ },
+ },
+ },
+ },
+ } );
+ } );
+} );
+
describe( 'getEntityRecordNonTransientEdits', () => {
it( 'should return an empty object when the entity does not have a loaded config.', () => {
const state = deepFreeze( {
- entities: { config: {}, data: {} },
+ entities: { config: [], data: {} },
} );
expect(
getEntityRecordNonTransientEdits( state, 'someKind', 'someName', 'someId' )
diff --git a/packages/editor/package.json b/packages/editor/package.json
index b2055f150bb3e4..fecbbc95429342 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -50,6 +50,7 @@
"@wordpress/viewport": "file:../viewport",
"@wordpress/wordcount": "file:../wordcount",
"classnames": "^2.2.5",
+ "equivalent-key-map": "^0.2.2",
"lodash": "^4.17.15",
"memize": "^1.0.5",
"react-autosize-textarea": "^3.0.2",
diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js
new file mode 100644
index 00000000000000..3c8d770d8d1064
--- /dev/null
+++ b/packages/editor/src/components/entities-saved-states/index.js
@@ -0,0 +1,104 @@
+/**
+ * External dependencies
+ */
+import { startCase } from 'lodash';
+import EquivalentKeyMap from 'equivalent-key-map';
+
+/**
+ * WordPress dependencies
+ */
+import { CheckboxControl, Modal, Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
+
+const EntitiesSavedStatesCheckbox = ( {
+ id,
+ name,
+ changes: { rawRecord },
+ checked,
+ setCheckedById,
+} ) => (
+ setCheckedById( id, nextChecked ) }
+ />
+);
+
+export default function EntitiesSavedStates( {
+ isOpen,
+ onRequestClose,
+ ignoredForSave = new EquivalentKeyMap(),
+} ) {
+ const entityRecordChangesByRecord = useSelect( ( select ) =>
+ select( 'core' ).getEntityRecordChangesByRecord()
+ );
+ const { saveEditedEntityRecord } = useDispatch( 'core' );
+
+ const [ checkedById, _setCheckedById ] = useState( () => new EquivalentKeyMap() );
+ const setCheckedById = ( id, checked ) =>
+ _setCheckedById( ( prevCheckedById ) => {
+ const nextCheckedById = new EquivalentKeyMap( prevCheckedById );
+ if ( checked ) {
+ nextCheckedById.set( id, true );
+ } else {
+ nextCheckedById.delete( id );
+ }
+ return nextCheckedById;
+ } );
+ const saveCheckedEntities = () => {
+ checkedById.forEach( ( _checked, id ) => {
+ if ( ! ignoredForSave.has( id ) ) {
+ saveEditedEntityRecord(
+ ...id.filter( ( s, i ) => i !== id.length - 1 || s !== 'undefined' )
+ );
+ }
+ } );
+ onRequestClose( checkedById );
+ };
+ return (
+ isOpen && (
+ onRequestClose() }
+ contentLabel={ __( 'Select items to save.' ) }
+ >
+ { Object.keys( entityRecordChangesByRecord ).map( ( changedKind ) =>
+ Object.keys( entityRecordChangesByRecord[ changedKind ] ).map(
+ ( changedName ) =>
+ Object.keys(
+ entityRecordChangesByRecord[ changedKind ][ changedName ]
+ ).map( ( changedKey ) => {
+ const id = [ changedKind, changedName, changedKey ];
+ return (
+
+ );
+ } )
+ )
+ ) }
+
+
+ )
+ );
+}
diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss
new file mode 100644
index 00000000000000..6751b3ef2673b8
--- /dev/null
+++ b/packages/editor/src/components/entities-saved-states/style.scss
@@ -0,0 +1,5 @@
+.editor-entities-saved-states__save-button {
+ display: block;
+ margin-left: auto;
+ margin-right: 0;
+}
diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js
index d5af7f4ff443bf..ce7f2e60c528d0 100644
--- a/packages/editor/src/components/index.js
+++ b/packages/editor/src/components/index.js
@@ -13,6 +13,7 @@ export { default as TextEditorGlobalKeyboardShortcuts } from './global-keyboard-
export { default as EditorHistoryRedo } from './editor-history/redo';
export { default as EditorHistoryUndo } from './editor-history/undo';
export { default as EditorNotices } from './editor-notices';
+export { default as EntitiesSavedStates } from './entities-saved-states';
export { default as ErrorBoundary } from './error-boundary';
export { default as LocalAutosaveMonitor } from './local-autosave-monitor';
export { default as PageAttributesCheck } from './page-attributes/check';
diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js
index b378fe7ae798df..de7b3069d17265 100644
--- a/packages/editor/src/components/post-publish-button/index.js
+++ b/packages/editor/src/components/post-publish-button/index.js
@@ -2,6 +2,9 @@
* External dependencies
*/
import { noop, get } from 'lodash';
+import classnames from 'classnames';
+import memoize from 'memize';
+import EquivalentKeyMap from 'equivalent-key-map';
/**
* WordPress dependencies
@@ -15,11 +18,25 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
+import EntitiesSavedStates from '../entities-saved-states';
import PublishButtonLabel from './label';
+
export class PostPublishButton extends Component {
constructor( props ) {
super( props );
this.buttonNode = createRef();
+
+ this.createOnClick = this.createOnClick.bind( this );
+ this.closeEntitiesSavedStates = this.closeEntitiesSavedStates.bind( this );
+
+ this.state = {
+ entitiesSavedStatesCallback: false,
+ };
+ this.createIgnoredForSave = memoize(
+ ( postType, postId ) =>
+ new EquivalentKeyMap( [ [ [ 'postType', postType, String( postId ) ], true ] ] ),
+ { maxSize: 1 }
+ );
}
componentDidMount() {
if ( this.props.focusOnMount ) {
@@ -27,6 +44,34 @@ export class PostPublishButton extends Component {
}
}
+ createOnClick( callback ) {
+ return ( ...args ) => {
+ const { hasNonPostEntityChanges } = this.props;
+ if ( hasNonPostEntityChanges ) {
+ // The modal for multiple entity saving will open,
+ // hold the callback for saving/publishing the post
+ // so that we can call it if the post entity is checked.
+ this.setState( {
+ entitiesSavedStatesCallback: () => callback( ...args ),
+ } );
+ return noop;
+ }
+
+ return callback( ...args );
+ };
+ }
+
+ closeEntitiesSavedStates( savedById ) {
+ const { postType, postId } = this.props;
+ const { entitiesSavedStatesCallback } = this.state;
+ this.setState( { entitiesSavedStatesCallback: false }, () => {
+ if ( savedById && savedById.has( [ 'postType', postType, String( postId ) ] ) ) {
+ // The post entity was checked, call the held callback from `createOnClick`.
+ entitiesSavedStatesCallback();
+ }
+ } );
+ }
+
render() {
const {
forceIsDirty,
@@ -45,7 +90,14 @@ export class PostPublishButton extends Component {
onSubmit = noop,
onToggle,
visibility,
+ hasNonPostEntityChanges,
+ postType,
+ postId,
} = this.props;
+ const {
+ entitiesSavedStatesCallback,
+ } = this.state;
+
const isButtonDisabled =
isSaving ||
forceIsSaving ||
@@ -88,34 +140,53 @@ export class PostPublishButton extends Component {
};
const buttonProps = {
- 'aria-disabled': isButtonDisabled,
+ 'aria-disabled': isButtonDisabled && ! hasNonPostEntityChanges,
className: 'editor-post-publish-button',
isBusy: isSaving && isPublished,
isPrimary: true,
- onClick: onClickButton,
+ onClick: this.createOnClick( onClickButton ),
};
const toggleProps = {
- 'aria-disabled': isToggleDisabled,
+ 'aria-disabled': isToggleDisabled && ! hasNonPostEntityChanges,
'aria-expanded': isOpen,
className: 'editor-post-publish-panel__toggle',
isBusy: isSaving && isPublished,
isPrimary: true,
- onClick: onClickToggle,
+ onClick: this.createOnClick( onClickToggle ),
};
const toggleChildren = isBeingScheduled ? __( 'Schedule…' ) : __( 'Publish…' );
- const buttonChildren = ;
+ const buttonChildren = (
+
+ );
const componentProps = isToggle ? toggleProps : buttonProps;
const componentChildren = isToggle ? toggleChildren : buttonChildren;
return (
-
+ <>
+
+
+ >
);
}
}
@@ -132,6 +203,8 @@ export default compose( [
isPostSavingLocked,
getCurrentPost,
getCurrentPostType,
+ getCurrentPostId,
+ hasNonPostEntityChanges,
} = select( 'core/editor' );
return {
isSaving: isSavingPost(),
@@ -143,6 +216,8 @@ export default compose( [
isPublished: isCurrentPostPublished(),
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
postType: getCurrentPostType(),
+ postId: getCurrentPostId(),
+ hasNonPostEntityChanges: hasNonPostEntityChanges(),
};
} ),
withDispatch( ( dispatch ) => {
diff --git a/packages/editor/src/components/post-publish-button/label.js b/packages/editor/src/components/post-publish-button/label.js
index fe9081c971c325..5373d39322558f 100644
--- a/packages/editor/src/components/post-publish-button/label.js
+++ b/packages/editor/src/components/post-publish-button/label.js
@@ -17,6 +17,7 @@ export function PublishButtonLabel( {
isPublishing,
hasPublishAction,
isAutosaving,
+ hasNonPostEntityChanges,
} ) {
if ( isPublishing ) {
return __( 'Publishing…' );
@@ -27,14 +28,16 @@ export function PublishButtonLabel( {
}
if ( ! hasPublishAction ) {
- return __( 'Submit for Review' );
+ return hasNonPostEntityChanges ?
+ __( 'Submit for Review…' ) :
+ __( 'Submit for Review' );
} else if ( isPublished ) {
- return __( 'Update' );
+ return hasNonPostEntityChanges ? __( 'Update…' ) : __( 'Update' );
} else if ( isBeingScheduled ) {
- return __( 'Schedule' );
+ return hasNonPostEntityChanges ? __( 'Schedule…' ) : __( 'Schedule' );
}
- return __( 'Publish' );
+ return hasNonPostEntityChanges ? __( 'Publish…' ) : __( 'Publish' );
}
export default compose( [
diff --git a/packages/editor/src/components/post-publish-button/style.scss b/packages/editor/src/components/post-publish-button/style.scss
new file mode 100644
index 00000000000000..11aed2ab58e3ed
--- /dev/null
+++ b/packages/editor/src/components/post-publish-button/style.scss
@@ -0,0 +1,8 @@
+.editor-post-publish-button__button.has-changes-dot::before {
+ background: currentcolor;
+ border-radius: 4px;
+ content: "";
+ height: 8px;
+ margin: auto 5px auto -3px;
+ width: 8px;
+}
diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js
index 9cabf7bff4708e..d8777cb5b590a4 100644
--- a/packages/editor/src/store/selectors.js
+++ b/packages/editor/src/store/selectors.js
@@ -134,6 +134,65 @@ export const isEditedPostDirty = createRegistrySelector( ( select ) => ( state )
return false;
} );
+/**
+ * Returns true if there are unsaved edits for entities other than
+ * the editor's post, and false otherwise.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {boolean} Whether there are edits or not.
+ */
+export const hasNonPostEntityChanges = createRegistrySelector(
+ ( select ) => ( state ) => {
+ const enableFullSiteEditing = getEditorSettings( state )
+ .__experimentalEnableFullSiteEditing;
+ if ( ! enableFullSiteEditing ) {
+ return false;
+ }
+
+ const entityRecordChangesByRecord = select(
+ 'core'
+ ).getEntityRecordChangesByRecord();
+ const changedKinds = Object.keys( entityRecordChangesByRecord );
+ if (
+ changedKinds.length > 1 ||
+ ( changedKinds.length === 1 && ! entityRecordChangesByRecord.postType )
+ ) {
+ // Return true if there is more than one edited entity kind
+ // or the edited entity kind is not the editor's post's kind.
+ return true;
+ } else if ( ! entityRecordChangesByRecord.postType ) {
+ // Don't continue if there are no edited entity kinds.
+ return false;
+ }
+
+ const { type, id } = getCurrentPost( state );
+ const changedPostTypes = Object.keys( entityRecordChangesByRecord.postType );
+ if (
+ changedPostTypes.length > 1 ||
+ ( changedPostTypes.length === 1 &&
+ ! entityRecordChangesByRecord.postType[ type ] )
+ ) {
+ // Return true if there is more than one edited post type
+ // or the edited entity's post type is not the editor's post's post type.
+ return true;
+ }
+
+ const changedPosts = Object.keys( entityRecordChangesByRecord.postType[ type ] );
+ if (
+ changedPosts.length > 1 ||
+ ( changedPosts.length === 1 &&
+ ! entityRecordChangesByRecord.postType[ type ][ id ] )
+ ) {
+ // Return true if there is more than one edited post
+ // or the edited post is not the editor's post.
+ return true;
+ }
+
+ return false;
+ }
+);
+
/**
* Returns true if there are no unsaved values for the current edit session and
* if the currently edited post is new (has never been saved before).
diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js
index f379c7eaae47ec..a6608f28b6cd68 100644
--- a/packages/editor/src/store/test/selectors.js
+++ b/packages/editor/src/store/test/selectors.js
@@ -33,6 +33,13 @@ selectorNames.forEach( ( name ) => {
return state.currentPost;
},
+ getEntityRecordChangesByRecord() {
+ return (
+ state.getEntityRecordChangesByRecord &&
+ state.getEntityRecordChangesByRecord()
+ );
+ },
+
getEntityRecordEdits() {
const present = state.editor && state.editor.present;
let edits = present && present.edits;
@@ -123,6 +130,7 @@ const {
isEditedPostNew,
hasChangedContent,
isEditedPostDirty,
+ hasNonPostEntityChanges,
isCleanNewPost,
getCurrentPost,
getCurrentPostId,
@@ -444,6 +452,69 @@ describe( 'selectors', () => {
} );
} );
+ describe( 'hasNonPostEntityChanges', () => {
+ it( 'should return false if the full site editing experiment is disabled.', () => {
+ const state = {
+ currentPost: { id: 1, type: 'post' },
+ editorSettings: {
+ __experimentalEnableFullSiteEditing: false,
+ },
+ getEntityRecordChangesByRecord() {
+ return { someKind: { someName: { someKey: {} } } };
+ },
+ };
+ expect( hasNonPostEntityChanges( state ) ).toBe( false );
+ } );
+ it( 'should return true if there are changes to an arbitrary entity', () => {
+ const state = {
+ currentPost: { id: 1, type: 'post' },
+ editorSettings: {
+ __experimentalEnableFullSiteEditing: true,
+ },
+ getEntityRecordChangesByRecord() {
+ return { someKind: { someName: { someKey: {} } } };
+ },
+ };
+ expect( hasNonPostEntityChanges( state ) ).toBe( true );
+ } );
+ it( 'should return false if there are only changes for the current post', () => {
+ const state = {
+ currentPost: { id: 1, type: 'post' },
+ editorSettings: {
+ __experimentalEnableFullSiteEditing: true,
+ },
+ getEntityRecordChangesByRecord() {
+ return { postType: { post: { 1: {} } } };
+ },
+ };
+ expect( hasNonPostEntityChanges( state ) ).toBe( false );
+ } );
+ it( 'should return true if there are changes to multiple posts', () => {
+ const state = {
+ currentPost: { id: 1, type: 'post' },
+ editorSettings: {
+ __experimentalEnableFullSiteEditing: true,
+ },
+ getEntityRecordChangesByRecord() {
+ return { postType: { post: { 1: {}, 2: {} } } };
+ },
+ };
+ expect( hasNonPostEntityChanges( state ) ).toBe( true );
+ } );
+ it( 'should return true if there are changes to multiple posts of different post types', () => {
+ const state = {
+ currentPost: { id: 1, type: 'post' },
+ editorSettings: {
+ __experimentalEnableFullSiteEditing: true,
+ },
+ getEntityRecordChangesByRecord() {
+ return { postType: { post: { 1: {} }, wp_template: { 1: {} } } };
+ },
+ };
+ expect( hasNonPostEntityChanges( state ) ).toBe( true );
+ } );
+ } );
+
describe( 'isCleanNewPost', () => {
it( 'should return true when the post is not dirty and has not been saved before', () => {
const state = {
diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss
index 949f751bd72af5..83a75edd9013ac 100644
--- a/packages/editor/src/style.scss
+++ b/packages/editor/src/style.scss
@@ -1,6 +1,7 @@
@import "./components/autocompleters/style.scss";
@import "./components/document-outline/style.scss";
@import "./components/editor-notices/style.scss";
+@import "./components/entities-saved-states/style.scss";
@import "./components/error-boundary/style.scss";
@import "./components/page-attributes/style.scss";
@import "./components/post-excerpt/style.scss";
@@ -9,6 +10,7 @@
@import "./components/post-last-revision/style.scss";
@import "./components/post-locked-modal/style.scss";
@import "./components/post-permalink/style.scss";
+@import "./components/post-publish-button/style.scss";
@import "./components/post-publish-panel/style.scss";
@import "./components/post-saved-state/style.scss";
@import "./components/post-taxonomies/style.scss";